diff options
| author | Aaron Bohy <aab@odoo.com> | 2016-04-29 15:22:23 +0200 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2016-05-03 10:34:41 +0200 |
| commit | 2264378739520fdc8b4c3967eebd4b35fff9f7fc (patch) | |
| tree | 5b6cb0eb3f7d66f4a1eecf8bd7f2c39a08887ca0 /addons/mail | |
| parent | 29483ffe2eed65eee66ea49053ae5c8fda221a6b (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.py | 2 | ||||
| -rw-r--r-- | addons/mail/models/mail_channel.py | 2 | ||||
| -rw-r--r-- | addons/mail/models/mail_shortcode.py | 20 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chat_manager.js | 19 | ||||
| -rw-r--r-- | addons/mail/static/src/js/composer.js | 71 | ||||
| -rw-r--r-- | addons/mail/static/src/less/composer.less | 12 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/composer.xml | 37 |
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 && (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> |
