summaryrefslogtreecommitdiff
path: root/addons/mail
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2016-04-29 15:22:23 +0200
committerAaron Bohy <aab@odoo.com>2016-05-03 10:34:41 +0200
commit2264378739520fdc8b4c3967eebd4b35fff9f7fc (patch)
tree5b6cb0eb3f7d66f4a1eecf8bd7f2c39a08887ca0 /addons/mail
parent29483ffe2eed65eee66ea49053ae5c8fda221a6b (diff)
[ADD] im_livechat,mail: canned responses support
Use the mention mechanism of the composer to autocomplete on canned responses (keyword :) with fuzzy search. Directly perform the string replacement in the composer, instead of server-side as before. Add a menu item to access an editable list view to create and edit canned responses. Add two canned responses as demo data.
Diffstat (limited to 'addons/mail')
-rw-r--r--addons/mail/controllers/main.py2
-rw-r--r--addons/mail/models/mail_channel.py2
-rw-r--r--addons/mail/models/mail_shortcode.py20
-rw-r--r--addons/mail/static/src/js/chat_manager.js19
-rw-r--r--addons/mail/static/src/js/composer.js71
-rw-r--r--addons/mail/static/src/less/composer.less12
-rw-r--r--addons/mail/static/src/xml/composer.xml37
7 files changed, 116 insertions, 47 deletions
diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py
index 6aa5d854aaa..3e763c72b39 100644
--- a/addons/mail/controllers/main.py
+++ b/addons/mail/controllers/main.py
@@ -243,7 +243,7 @@ class MailController(http.Controller):
'starred_counter': request.env['res.partner'].get_starred_count(),
'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
- 'emoji': request.env['mail.shortcode'].sudo().search_read([('shortcode_type', '=', 'image')], ['source', 'substitution', 'description']),
+ 'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['shortcode_type', 'source', 'substitution', 'description']),
'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mail_channel_menu_root_chat'),
}
return values
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 41fc8e0d551..02607bfcbc3 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -213,8 +213,6 @@ class Channel(models.Model):
def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs):
# auto pin 'direct_message' channel partner
self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
- # apply shortcode (text only) subsitution
- body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text')
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs)
return message
diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py
index 478fb7be2ef..8c659a1f134 100644
--- a/addons/mail/models/mail_shortcode.py
+++ b/addons/mail/models/mail_shortcode.py
@@ -15,12 +15,12 @@ class MailShortcode(models.Model):
_name = 'mail.shortcode'
_description = 'Canned Response / Shortcode'
- source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages")
- substitution = fields.Char('Substitution', required=True, index=True, help="The escaped html code replacing the shortcut")
+ source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replaced in the Chat Messages")
+ substitution = fields.Text('Substitution', required=True, index=True, help="The escaped html code replacing the shortcut")
description = fields.Char('Description')
shortcode_type = fields.Selection([('image', 'Smiley'), ('text', 'Canned Response')], required=True, default='text',
help="* Smiley are only used for HTML code to display an image "\
- "* Text (default value) is use to substitute text with another text")
+ "* Text (default value) is used to substitute text with another text")
@api.model
def create(self, values):
@@ -43,17 +43,3 @@ class MailShortcode(models.Model):
if is_img_tag:
return substitution
return cgi.escape(substitution)
-
- @api.model
- def apply_shortcode(self, message, shortcode_type=None):
- """ Apply the substitution on the given text
- :param message : the text where the shortcode will be applied
- :param shortcode_type : shortcode type that should be applied during the substitution
- """
- domain = []
- if shortcode_type:
- domain.append(('shortcode_type', '=', shortcode_type))
- for shortcode in self.search(domain):
- regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source)
- message = re.sub(regex, " " + shortcode.substitution + " ", message)
- return message
diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js
index 69088701bc1..34644aee812 100644
--- a/addons/mail/static/src/js/chat_manager.js
+++ b/addons/mail/static/src/js/chat_manager.js
@@ -34,6 +34,7 @@ var emoji_substitutions = {};
var needaction_counter = 0;
var starred_counter = 0;
var mention_partner_suggestions = [];
+var canned_responses = [];
var discuss_menu_id;
var global_unread_counter = 0;
var pinned_dm_partners = []; // partner_ids we have a pinned DM with
@@ -757,6 +758,10 @@ var chat_manager = {
return channel.members_deferred;
},
+ get_canned_responses: function () {
+ return canned_responses;
+ },
+
get_emojis: function() {
return emojis;
},
@@ -988,12 +993,18 @@ function init () {
needaction_counter = result.needaction_inbox_counter;
starred_counter = result.starred_counter;
mention_partner_suggestions = result.mention_partner_suggestions;
- emojis = result.emoji;
- _.each(emojis, function(emoji) {
- emoji_substitutions[_.escape(emoji.source)] = emoji.substitution;
- });
discuss_menu_id = result.menu_id;
+ // Shortcodes: canned responses and emojis
+ _.each(result.shortcodes, function (s) {
+ if (s.shortcode_type === 'text') {
+ canned_responses.push(_.pick(s, ['id', 'source', 'substitution']));
+ } else {
+ emojis.push(_.pick(s, ['id', 'source', 'substitution', 'description']));
+ emoji_substitutions[_.escape(s.source)] = s.substitution;
+ }
+ });
+
bus.start_polling();
});
}
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index af9efe25859..3306a9ed0b0 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -18,6 +18,7 @@ var NON_BREAKING_SPACE = '\u00a0';
var MENTION_PARTNER_DELIMITER = '@';
var MENTION_CHANNEL_DELIMITER = '#';
+var MENTION_CANNED_RESPONSE_DELIMITER = ':';
// The MentionManager allows the Composer to register listeners. For each
// listener, it detects if the user is currently typing a mention (starting by a
@@ -59,8 +60,12 @@ var MentionManager = Widget.extend({
var selected_suggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) {
return s.id === id;
});
- // replace white spaces with non-breaking spaces to facilitate mentions detection in text
- selected_suggestion.name = selected_suggestion.name.replace(/ /g, NON_BREAKING_SPACE);
+ var substitution = selected_suggestion.substitution;
+ if (!substitution) { // no substitution string given, so use the mention name instead
+ // replace white spaces with non-breaking spaces to facilitate mentions detection in text
+ selected_suggestion.name = selected_suggestion.name.replace(/ /g, NON_BREAKING_SPACE);
+ substitution = this.active_listener.delimiter + selected_suggestion.name;
+ }
var get_mention_index = function (matches, cursor_position) {
for (var i=0; i<matches.length; i++) {
if (cursor_position <= matches[i].index) {
@@ -84,10 +89,11 @@ var MentionManager = Widget.extend({
var cursor_position = this._get_selection_positions().start;
var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
var text_right = text_input.substring(cursor_position, text_input.length);
- var text_input_new = text_left + this.active_listener.delimiter + selected_suggestion.name + ' ' + text_right;
+ var text_input_new = text_left + substitution + ' ' + text_right;
this.composer.$input.val(text_input_new);
- this._set_cursor_position(text_left.length+selected_suggestion.name.length+2);
+ this._set_cursor_position(text_left.length+substitution.length+2);
this.set('mention_suggestions', []);
+ this.composer.focus(); // to trigger autoresize
},
// Public API
@@ -95,11 +101,16 @@ var MentionManager = Widget.extend({
* Registers a new listener, described by an object containing the following keys
* @param {char} [delimiter] the mention delimiter
* @param {function} [fetch_callback] the callback to fetch mention suggestions
- * @param {string} [model] the model used for redirection
- * @param {string} [redirect_classname] the classname of the <a> wrapping the mention
+ * @param {boolean} [generate_links] true to wrap mentions in <a> links
+ * @param {string} [model] (optional) the model used for redirection
+ * @param {string} [redirect_classname] (optional) the classname of the <a> wrapping the mention
+ * @param {array} [selection] (optional) initial mentions for each listener
+ * @param {string} [suggestion_template] the template of suggestions' dropdown
*/
register: function (listener) {
this.listeners.push(_.defaults(listener, {
+ model: '',
+ redirect_classname: '',
selection: [],
}));
},
@@ -202,6 +213,9 @@ var MentionManager = Widget.extend({
var base_href = session.url("/web");
var mention_link = "<a href='%s' class='%s' data-oe-id='%s' data-oe-model='%s' target='_blank'>%s%s</a>";
_.each(this.listeners, function (listener) {
+ if (!listener.generate_links) {
+ return;
+ }
var selection = listener.selection;
if (selection.length) {
var matches = self._get_match(s, listener);
@@ -266,12 +280,29 @@ var MentionManager = Widget.extend({
return result;
},
_render_suggestions: function () {
- if (_.flatten(this.get('mention_suggestions')).length) {
- this.$el.html(QWeb.render('mail.ChatComposer.MentionSuggestions', {
- suggestions: this.get('mention_suggestions'),
+ var suggestions = [];
+ if (_.find(this.get('mention_suggestions'), _.isArray)) {
+ // Array of arrays -> Flatten and insert dividers between groups
+ var insert_divider = false;
+ _.each(this.get('mention_suggestions'), function (suggestion_group) {
+ if (suggestion_group.length > 0) {
+ if (insert_divider) {
+ suggestions.push({ divider: true });
+ }
+ suggestions = suggestions.concat(suggestion_group);
+ insert_divider = true;
+ }
+ });
+ } else {
+ suggestions = this.get('mention_suggestions');
+ }
+ if (suggestions.length) {
+ this.$el.html(QWeb.render(this.active_listener.suggestion_template, {
+ suggestions: suggestions,
}));
this.$el
.addClass('open')
+ .find('ul').css("max-width", this.composer.$input.width())
.find('.o_mention_proposition').first().addClass('active');
this.open = true;
} else {
@@ -346,16 +377,26 @@ var BasicComposer = Widget.extend({
this.mention_manager.register({
delimiter: MENTION_PARTNER_DELIMITER,
fetch_callback: this.mention_fetch_partners.bind(this),
+ generate_links: true,
model: 'res.partner',
redirect_classname: 'o_mail_redirect',
selection: this.options.default_mention_selections[MENTION_PARTNER_DELIMITER],
+ suggestion_template: 'mail.MentionPartnerSuggestions',
});
this.mention_manager.register({
delimiter: MENTION_CHANNEL_DELIMITER,
fetch_callback: this.mention_fetch_channels.bind(this),
+ generate_links: true,
model: 'mail.channel',
redirect_classname: 'o_channel_redirect',
selection: this.options.default_mention_selections[MENTION_CHANNEL_DELIMITER],
+ suggestion_template: 'mail.MentionChannelSuggestions',
+ });
+ this.mention_manager.register({
+ delimiter: MENTION_CANNED_RESPONSE_DELIMITER,
+ fetch_callback: this.mention_get_canned_responses.bind(this),
+ selection: this.options.default_mention_selections[MENTION_CANNED_RESPONSE_DELIMITER],
+ suggestion_template: 'mail.MentionCannedResponseSuggestions',
});
// Emojis
@@ -621,8 +662,8 @@ var BasicComposer = Widget.extend({
_.each(prefetched_partners, function (partners) {
if (limit > 0) {
var filtered_partners = _.filter(partners, function (partner) {
- return partner.email && partner.email.search(search_regexp) !== -1 ||
- partner.name && utils.unaccent(partner.name).search(search_regexp) !== -1;
+ return partner.email && search_regexp.test(partner.email) ||
+ partner.name && search_regexp.test(utils.unaccent(partner.name));
});
if (filtered_partners.length) {
suggestions.push(filtered_partners.slice(0, limit));
@@ -640,6 +681,14 @@ var BasicComposer = Widget.extend({
return suggestions;
});
},
+ mention_get_canned_responses: function (search) {
+ var canned_responses = chat_manager.get_canned_responses();
+ var matches = fuzzy.filter(utils.unaccent(search), _.pluck(canned_responses, 'source'));
+ var indexes = _.pluck(matches.slice(0, this.options.mention_fetch_limit), 'index');
+ return _.map(indexes, function (i) {
+ return canned_responses[i];
+ });
+ },
mention_set_prefetched_partners: function (prefetched_partners) {
this.mention_prefetched_partners = prefetched_partners;
},
diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less
index 31ae4725ef0..eb5be1225ac 100644
--- a/addons/mail/static/src/less/composer.less
+++ b/addons/mail/static/src/less/composer.less
@@ -14,6 +14,18 @@
width: 100%;
resize: none;
}
+ .o_composer_mention_dropdown .o_mention_proposition > a {
+ .o-text-overflow();
+ width: 100%;
+ padding: 3px 15px;
+ .o_mention_name {
+ font-weight: 500;
+ }
+ .o_mention_info {
+ font-style: italic;
+ color: gray;
+ }
+ }
}
.o_composer_attachments_list {
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
index a75708401ce..9e248755a56 100644
--- a/addons/mail/static/src/xml/composer.xml
+++ b/addons/mail/static/src/xml/composer.xml
@@ -46,21 +46,34 @@
</t>
</t>
- <t t-name="mail.ChatComposer.MentionSuggestions">
+ <t t-name="mail.AbstractMentionSuggestions">
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
- <t t-set="show_divider" t-value="false"/>
- <t t-foreach="suggestions" t-as="group">
- <t t-foreach="group" t-as="suggestion">
- <li t-if="show_divider &amp;&amp; (suggestion_index === 0)" class="divider"/>
- <t t-set="show_divider" t-value="true"/>
- <li class="o_mention_proposition" t-att-data-id="suggestion.id">
- <a href="#">
- <t t-esc="suggestion.name"/> <t t-if="suggestion.email">(<t t-esc="suggestion.email"/>)</t>
- </a>
- </li>
- </t>
+ <t t-foreach="suggestions" t-as="suggestion">
+ <li t-if="suggestion.divider" class="divider"/>
+ <li t-if="!suggestion.divider" class="o_mention_proposition" t-att-data-id="suggestion.id">
+ <a href="#"/>
+ </li>
</t>
</ul>
</t>
+ <t t-name="mail.MentionPartnerSuggestions" t-extend="mail.AbstractMentionSuggestions">
+ <t t-jquery=".o_mention_proposition a" t-operation="append">
+ <span class="o_mention_name"><t t-esc="suggestion.name"/></span>
+ <t t-if="suggestion.email">
+ <span class="o_mention_info">(<t t-esc="suggestion.email"/>)</span>
+ </t>
+ </t>
+ </t>
+ <t t-name="mail.MentionChannelSuggestions" t-extend="mail.AbstractMentionSuggestions">
+ <t t-jquery=".o_mention_proposition a" t-operation="append">
+ <span class="o_mention_name"><t t-esc="suggestion.name"/></span>
+ </t>
+ </t>
+ <t t-name="mail.MentionCannedResponseSuggestions" t-extend="mail.AbstractMentionSuggestions">
+ <t t-jquery=".o_mention_proposition a" t-operation="append">
+ <span class="o_mention_name"><t t-esc="suggestion.source"/></span>
+ <span class="o_mention_info"><t t-esc="suggestion.substitution"/></span>
+ </t>
+ </t>
</templates>