Compare commits

...

22 commits

Author SHA1 Message Date
Muhsin
ea5a25934d Skip code blocks 2022-12-27 15:48:48 +05:30
Muhsin
9d12826cb0 Add variable support in add canned response 2022-12-26 16:18:34 +05:30
Muhsin
ea69d46b9e Show undefined variable warning before sending messaeg 2022-12-26 15:16:55 +05:30
Muhsin
efa3d41ea6 Code cleanup 2022-12-23 16:34:06 +05:30
Muhsin
0f8e5c6fe1 Merge branch 'feat/support-variables-in-message' of https://github.com/chatwoot/chatwoot into feat/support-variables-in-message 2022-12-23 16:32:57 +05:30
Muhsin Keloth
c3135d1d2f
Merge branch 'develop' into feat/support-variables-in-message 2022-12-23 16:31:23 +05:30
Muhsin
3848f38dde Add variable list component 2022-12-23 16:30:18 +05:30
Muhsin
4d763b36a3 Add message helpers 2022-12-23 15:36:55 +05:30
Muhsin
8dd1f588ba Add first name and last name in user drop 2022-12-23 15:36:11 +05:30
Muhsin
322b253c97 Add first name and last name support in liquid variables 2022-12-23 11:56:17 +05:30
Muhsin Keloth
289ad1e7f3
Merge branch 'develop' into feat/support-variables-in-message 2022-12-22 22:54:44 +05:30
Muhsin Keloth
9be03bdbe8
Merge branch 'develop' into feat/support-variables-in-message 2022-12-19 23:40:24 +05:30
Sivin Varghese
7f0d2097cb
Merge branch 'develop' into feat/support-variables-in-message 2022-12-19 10:51:48 +05:30
Sojan
ddd6750df5 fix: codeblocks 2022-12-16 18:31:24 +05:30
Sojan
faaf409d09 fix: codeblocks 2022-12-16 18:29:26 +05:30
Sojan
ea25651bc6 chore: refactor 2022-12-16 17:34:52 +05:30
Muhsin Keloth
c4682eb57c
Merge branch 'develop' into feat/support-variables-in-message 2022-12-16 00:03:29 +05:30
Muhsin
e03285e702 Parse only if message is outgoing 2022-12-16 00:01:30 +05:30
Muhsin
f65d45a01a Add more specs 2022-12-15 23:39:54 +05:30
Muhsin Keloth
b01f0744d7
Merge branch 'develop' into feat/support-variables-in-message 2022-12-15 21:52:42 +05:30
Muhsin
5fb282dcbd Add specs 2022-12-15 21:48:08 +05:30
Muhsin
7892b039a7 Add liquid support 2022-12-15 21:10:44 +05:30
17 changed files with 688 additions and 20 deletions

17
app/drops/contact_drop.rb Normal file
View file

@ -0,0 +1,17 @@
class ContactDrop < BaseDrop
def email
@obj.try(:email)
end
def phone_number
@obj.try(:phone_number)
end
def first_name
@obj.try(:name).try(:split, ' ').try(:first)
end
def last_name
@obj.try(:name).try(:split, ' ').try(:last) if @obj.try(:name).try(:split, ' ').try(:size) > 1
end
end

View file

@ -2,4 +2,12 @@ class UserDrop < BaseDrop
def available_name
@obj.try(:available_name)
end
def first_name
@obj.try(:name).try(:split, ' ').try(:first)
end
def last_name
@obj.try(:name).try(:split, ' ').try(:last) if @obj.try(:name).try(:split, ' ').try(:size) > 1
end
end

View file

@ -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>
@ -34,6 +39,7 @@ import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
import TagAgents from '../conversation/TagAgents';
import CannedResponse from '../conversation/CannedResponse';
import VariableList from '../conversation/VariableList';
const TYPING_INDICATOR_IDLE_TIME = 4000;
@ -50,6 +56,7 @@ import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
import { replaceVariablesInMessage } from 'dashboard/helper/messageHelper';
const createState = (content, placeholder, plugins = []) => {
return EditorState.create({
@ -64,7 +71,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: '' },
@ -74,13 +81,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,
@ -92,6 +104,14 @@ export default {
defaultMarkdownSerializer
).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 [];
@ -111,6 +131,7 @@ export default {
this.range = args.range;
this.mentionSearchKey = args.text.replace('@', '');
return false;
},
onExit: () => {
@ -150,6 +171,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;
},
}),
];
},
},
@ -277,17 +326,22 @@ export default {
},
insertCannedResponse(cannedItem) {
const updatedCannedResponse = replaceVariablesInMessage({
message: cannedItem,
variables: this.variables,
});
if (!this.editorView) {
return null;
}
let from = this.range.from - 1;
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
cannedItem
updatedCannedResponse
);
if (node.childCount === 1) {
node = this.editorView.state.schema.text(cannedItem);
node = this.editorView.state.schema.text(updatedCannedResponse);
from = this.range.from;
}
@ -302,6 +356,34 @@ export default {
tr.scrollIntoView();
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.showCannedMenu = false;
return false;
},
insertVariable(variable) {
if (!this.editorView) {
return null;
}
let from = this.range.from - 1;
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
variable
);
if (node.childCount === 1) {
node = this.editorView.state.schema.text(`{{ ${variable} }}`);
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();
tr.scrollIntoView();
return false;
},

View file

@ -63,6 +63,8 @@
:placeholder="messagePlaceHolder"
:update-selection-with="updateEditorSelectionWith"
:min-height="4"
:enable-variables="true"
:variables="messageVariables"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@ -125,6 +127,12 @@
@on-send="onSendWhatsAppReply"
@cancel="hideWhatsappTemplatesModal"
/>
<woot-confirm-modal
ref="confirmDialog"
:title="$t('CONVERSATION.REPLYBOX.UNDEFINED_VARIABLES.TITLE')"
:description="undefinedVariableMessage"
/>
</div>
</template>
@ -152,6 +160,10 @@ import {
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
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';
@ -224,6 +236,7 @@ export default {
doAutoSaveDraft: () => {},
showWhatsAppTemplatesModal: false,
updateEditorSelectionWith: '',
undefinedVariableMessage: '',
};
},
computed: {
@ -470,6 +483,12 @@ export default {
}
return AUDIO_FORMATS.OGG;
},
messageVariables() {
const variables = getMessageVariables({
conversation: this.currentChat,
});
return variables;
},
},
watch: {
currentChat(conversation) {
@ -665,7 +684,7 @@ export default {
};
this.assignedAgent = selfAssign;
},
async onSendReply() {
confirmOnSendReply() {
if (this.isReplyButtonDisabled) {
return;
}
@ -686,6 +705,25 @@ 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 = `You have ${undefinedVariablesCount} undefined variables in your message: ${undefinedVariables.join(
', '
)}. Would you like to send the message anyway?`;
const ok = await this.$refs.confirmDialog.showConfirmation();
if (ok) {
this.confirmOnSendReply();
}
} else {
this.confirmOnSendReply();
}
},
async sendMessage(messagePayload) {
try {
await this.$store.dispatch(

View file

@ -0,0 +1,33 @@
<template>
<variable :items="items" @mention-select="handleVariableClick" />
</template>
<script>
import { MESSAGE_VARIABLES } from 'shared/constants/messages';
import Variable from '../variable/Variable.vue';
export default {
components: { Variable },
props: {
searchKey: {
type: String,
default: '',
},
},
computed: {
items() {
return MESSAGE_VARIABLES.filter(variable => {
return (
variable.label.includes(this.searchKey) ||
variable.key.includes(this.searchKey)
);
});
},
},
methods: {
handleVariableClick(item = {}) {
this.$emit('click', item.key);
},
},
};
</script>

View file

@ -0,0 +1,86 @@
<template>
<ul
v-if="items.length"
class="vertical dropdown menu mention--box"
:style="{ top: getTopPadding() + 'rem' }"
>
<li
v-for="(item, index) in items"
:id="`mention-item-${index}`"
:key="item.key"
:class="{ active: index === selectedIndex }"
@click="onListItemSelection(index)"
@mouseover="onHover(index)"
>
<a class="text-truncate">
<strong>{{ item.label }}</strong>
</a>
</li>
</ul>
</template>
<script>
import mentionSelectionKeyboardMixin from '../mentions/mentionSelectionKeyboardMixin';
export default {
mixins: [mentionSelectionKeyboardMixin],
props: {
items: {
type: Array,
default: () => {},
},
},
data() {
return {
selectedIndex: 0,
};
},
watch: {
items(newItems) {
if (newItems.length < this.selectedIndex + 1) {
this.selectedIndex = 0;
}
},
},
methods: {
getTopPadding() {
if (this.items.length <= 4) {
return -(this.items.length * 2.9 + 1.7);
}
return -14;
},
handleKeyboardEvent(e) {
this.processKeyDownEvent(e);
this.$el.scrollTop = 29 * this.selectedIndex;
},
onHover(index) {
this.selectedIndex = index;
},
onListItemSelection(index) {
this.selectedIndex = index;
this.onSelect();
},
onSelect() {
this.$emit('mention-select', this.items[this.selectedIndex]);
},
},
};
</script>
<style scoped lang="scss">
.mention--box {
background: var(--white);
border-bottom: var(--space-small) solid var(--white);
border-top: 1px solid var(--color-border);
left: 0;
max-height: 14rem;
overflow: auto;
padding-top: var(--space-small);
position: absolute;
width: 100%;
z-index: 100;
.active a {
background: var(--w-500);
}
}
</style>

View file

@ -0,0 +1,63 @@
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 = ({ name }) => {
return name.split(' ')[0];
};
export const getLastName = ({ name }) => {
return name.split(' ').length > 1
? name.split(' ')[name.split(' ').length - 1]
: '';
};
export const getMessageVariables = ({ conversation }) => {
const {
meta: { assignee = {}, sender = {} },
id,
} = conversation;
return {
'contact.name': sender?.name,
'contact.first_name': getFirstName({ name: sender?.name }),
'contact.last_name': getLastName({ name: sender?.name }),
'contact.email': sender?.email,
'contact.phone': sender?.phone_number,
'conversation.id': id,
'agent.name': assignee?.name ? assignee?.name : '',
'agent.first_name': assignee?.name
? getFirstName({ name: assignee?.name })
: '',
'agent.last_name': assignee?.name
? getLastName({ name: assignee?.name })
: '',
'agent.email': assignee?.email ? assignee?.email : '',
};
};
export const getUndefinedVariablesInMessage = ({ message, variables }) => {
const messageWithOutCodeBlocks = skipCodeBlocks(message);
const matches = messageWithOutCodeBlocks.match(MESSAGE_VARIABLES_REGEX);
const undefinedVariables = [];
if (!matches) {
return [];
}
matches.forEach(match => {
const variable = match
.replace('{{', '')
.replace('}}', '')
.trim();
if (!variables[variable]) {
undefinedVariables.push(match);
}
});
return undefinedVariables;
};

View file

@ -0,0 +1,136 @@
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 name = 'John Doe';
expect(getFirstName({ name })).toBe('John');
});
it('returns the first name of the contact with multiple names', () => {
const name = 'John Doe Smith';
expect(getFirstName({ name })).toBe('John');
});
});
describe('#getLastName', () => {
it('returns the last name of the contact', () => {
const name = 'John Doe';
expect(getLastName({ name })).toBe('Doe');
});
it('returns the last name of the contact with multiple names', () => {
const name = 'John Doe Smith';
expect(getLastName({ name })).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}} ```';
expect(
getUndefinedVariablesInMessage({ message, variables }).length
).toEqual(1);
expect(getUndefinedVariablesInMessage({ message, variables })).toEqual(
expect.arrayContaining(['{{contact_name}}'])
);
});
});

View file

@ -132,6 +132,13 @@
"PLACEHOLDER": "Emails separated by commas",
"ERROR": "Please enter valid email addresses"
}
},
"UNDEFINED_VARIABLES": {
"TITLE": "Undefined variables",
"CONFIRM": {
"YES": "Send",
"CANCEL": "Cancel"
}
}
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",

View file

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

View file

@ -21,13 +21,17 @@
<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"
:placeholder="$t('CANNED_MGMT.ADD.FORM.CONTENT.PLACEHOLDER')"
@input="$v.content.$touch"
/>
<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')"
@blur="$v.content.$touch"
/>
</label>
</label>
</div>
<div class="modal-footer">
@ -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,19 @@ export default {
},
};
</script>
<style scoped lang="scss">
::v-deep .mention--box {
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
left: 0;
margin: auto;
right: 0;
top: 10rem !important;
width: 90%;
}
::v-deep .ProseMirror-menubar {
display: none;
}
</style>

View file

@ -18,13 +18,17 @@
<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"
:placeholder="$t('CANNED_MGMT.EDIT.FORM.CONTENT.PLACEHOLDER')"
@input="$v.content.$touch"
/>
<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')"
@blur="$v.content.$touch"
/>
</label>
</label>
</div>
<div class="modal-footer">
@ -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,18 @@ export default {
},
};
</script>
<style scoped lang="scss">
::v-deep .mention--box {
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
left: 0;
margin: auto;
right: 0;
top: 7rem !important;
width: 90%;
}
::v-deep .ProseMirror-menubar {
display: none;
}
</style>

View file

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

View file

@ -0,0 +1,36 @@
module Liquidable
extend ActiveSupport::Concern
included do
acts_as_taggable_on :labels
before_create :process_liquid_in_content
end
private
def message_drops
{
'contact' => ContactDrop.new(conversation.contact),
'agent' => UserDrop.new(sender),
'conversation' => ConversationDrop.new(conversation),
'inbox' => InboxDrop.new(inbox)
}
end
def liquid_processable_message?
content.present? && message_type == 'outgoing'
end
def process_liquid_in_content
return unless liquid_processable_message?
template = Liquid::Template.parse(modified_liquid_content)
self.content = template.render(message_drops)
end
def modified_liquid_content
# This regex is used to match the code blocks in the content
# We don't want to process liquid in code blocks
content.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
end
end

View file

@ -32,6 +32,7 @@
class Message < ApplicationRecord
include MessageFilterHelpers
include Liquidable
NUMBER_OF_PERMITTED_ATTACHMENTS = 15
before_validation :ensure_content_type

View file

@ -0,0 +1,66 @@
require 'rails_helper'
shared_examples_for 'liqudable' do
context 'when liquid is present in content' do
let(:contact) { create(:contact, name: 'john', phone_number: '+912883') }
let(:conversation) { create(:conversation, id: 1, contact: contact) }
context 'when message is incoming' do
let(:message) { build(:message, conversation: conversation, message_type: 'incoming') }
it 'will not process liquid in content' do
message.content = 'hey {{contact.name}} how are you?'
message.save!
expect(message.content).to eq 'hey {{contact.name}} how are you?'
end
end
context 'when message is outgoing' do
let(:message) { build(:message, conversation: conversation, message_type: 'outgoing') }
it 'set replaces liquid variables in message' do
message.content = 'hey {{contact.name}} how are you?'
message.save!
expect(message.content).to eq 'hey john how are you?'
end
it 'process liquid operators like default value' do
message.content = 'Can we send you an email at {{ contact.email | default: "default" }} ?'
message.save!
expect(message.content).to eq 'Can we send you an email at default ?'
end
it 'return empty string when value is not available' do
message.content = 'Can we send you an email at {{contact.email}}?'
message.save!
expect(message.content).to eq 'Can we send you an email at ?'
end
it 'will not process liquid tags in multiple code blocks' do
message.content = 'hey {{contact.name}} how are you? ``` code: {{contact.name}} ``` ``` code: {{contact.name}} ``` test'
message.save!
expect(message.content).to eq 'hey john how are you? ``` code: {{contact.name}} ``` ``` code: {{contact.name}} ``` test'
end
it 'will extract first name from contact name' do
message.content = 'hey {{contact.first_name}} how are you?'
message.save!
expect(message.content).to eq 'hey john how are you?'
end
it 'return empty last name when value is not available' do
message.content = 'hey {{contact.last_name}} how are you?'
message.save!
expect(message.content).to eq 'hey how are you?'
end
it 'will extract first name and last name from contact name' do
contact.name = 'john doe'
contact.save!
message.content = 'hey {{contact.first_name}} {{contact.last_name}} how are you?'
message.save!
expect(message.content).to eq 'hey john doe how are you?'
end
end
end
end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/liquidable_shared.rb'
RSpec.describe Message, type: :model do
context 'with validations' do
@ -9,6 +10,10 @@ RSpec.describe Message, type: :model do
it { is_expected.to validate_presence_of(:account_id) }
end
describe 'concerns' do
it_behaves_like 'liqudable'
end
describe '#reopen_conversation' do
let(:conversation) { create(:conversation) }
let(:message) { build(:message, message_type: :incoming, conversation: conversation) }