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 | |
| 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')
| -rw-r--r-- | addons/im_livechat/__openerp__.py | 1 | ||||
| -rw-r--r-- | addons/im_livechat/data/mail_shortcode_demo.xml | 18 | ||||
| -rw-r--r-- | addons/im_livechat/views/im_livechat_channel_views.xml | 37 | ||||
| -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 |
10 files changed, 172 insertions, 47 deletions
diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 030230dcc95..39d4c2bf10e 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -30,6 +30,7 @@ Help your customers with this chat, and analyse their feedback. ], 'demo': [ "data/im_livechat_channel_demo.xml", + 'data/mail_shortcode_demo.xml', ], 'depends': ["mail", "rating"], 'qweb': [ diff --git a/addons/im_livechat/data/mail_shortcode_demo.xml b/addons/im_livechat/data/mail_shortcode_demo.xml new file mode 100644 index 00000000000..748fb65ba47 --- /dev/null +++ b/addons/im_livechat/data/mail_shortcode_demo.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data noupdate="1"> + + <record id="mail_canned_response_hello" model="mail.shortcode"> + <field name="source">hello</field> + <field name="substitution">Hello. How may I help you?</field> + <field name="shortcode_type">text</field> + </record> + + <record id="mail_canned_response_bye" model="mail.shortcode"> + <field name="source">bye</field> + <field name="substitution">Thanks for your feedback. Good bye!</field> + <field name="shortcode_type">text</field> + </record> + + </data> +</openerp> diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 9b2b17259f0..b7a823f82e0 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -174,6 +174,35 @@ </field> </record> + <!-- Canned responses --> + <record id="im_livechat_canned_response_view_tree" model="ir.ui.view"> + <field name="name">im_livechat.canned_response.tree</field> + <field name="model">mail.shortcode</field> + <field name="arch" type="xml"> + <tree editable="bottom"> + <field name="source"/> + <field name="substitution"/> + </tree> + </field> + </record> + + <record id="im_livechat_canned_response_action" model="ir.actions.act_window"> + <field name="name">Canned Responses</field> + <field name="res_model">mail.shortcode</field> + <field name="view_mode">tree</field> + <field name="view_id" ref="im_livechat_canned_response_view_tree"/> + <field name="domain">[('shortcode_type','=','text')]</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click to create a new canned response. + </p><p> + Canned responses allow you to insert prewritten responses in + your messages by typing <i>:shortcut</i>. The shortcut is + replaced directly in your message, so that you can still edit + it before sending. + </p> + </field> + </record> <!-- Menu items --> <menuitem @@ -199,5 +228,13 @@ groups="im_livechat_group_user" sequence="10"/> + <menuitem + id="canned_responses" + name="Canned Responses" + parent="menu_livechat_root" + action="im_livechat_canned_response_action" + groups="im_livechat_group_user" + sequence="15"/> + </data> </openerp> 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> |
