From 7a0aecfca95067c35bcef536e84702421f4cfd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 10 Sep 2020 16:34:50 +0000 Subject: [PATCH] [FIX] im_livechat, mail, website_livechat: open chat website visitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix an issue with livechats not being considered in 'chat' filter of messaging menu. Task-Id 2282426 closes odoo/odoo#58468 X-original-commit: 631e52536964763bbfe857305f023e5e67084e95 Signed-off-by: Sébastien Theys (seb) --- .../messaging_menu/messaging_menu_tests.js | 100 ++++++++++++++++++ .../static/src/models/thread/thread.js | 6 ++ .../views/im_livechat_channel_templates.xml | 1 + .../notification_list/notification_list.js | 2 +- .../mail/static/src/models/thread/thread.js | 21 ++++ .../web/static/tests/helpers/mock_server.js | 18 ++++ .../models/website_visitor.py | 30 +++++- .../messaging_notification_handler.js | 32 ++++++ .../messaging_notification_handler_tests.js | 98 +++++++++++++++++ .../static/tests/helpers/mock_models.js | 33 ++++++ .../static/tests/helpers/mock_server.js | 52 +++++++++ addons/website_livechat/views/assets.xml | 6 +- .../views/website_livechat.xml | 5 - 13 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js create mode 100644 addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js create mode 100644 addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js create mode 100644 addons/website_livechat/static/tests/helpers/mock_models.js create mode 100644 addons/website_livechat/static/tests/helpers/mock_server.js diff --git a/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js b/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js new file mode 100644 index 00000000000..03c164c549d --- /dev/null +++ b/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js @@ -0,0 +1,100 @@ +odoo.define('im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('im_livechat', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('messaging_menu', {}, function () { +QUnit.module('messaging_menu_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + let { env, widget } = await start(Object.assign({}, params, { + data: this.data, + hasMessagingMenu: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('livechats should be in "chat" filter', async function (assert) { + assert.expect(7); + + this.data['mail.channel'].records.push({ + anonymous_name: "Visitor 11", + channel_type: 'livechat', + id: 11, + livechat_operator_id: this.data.currentPartnerId, + members: [this.data.currentPartnerId, this.data.publicPartnerId], + }); + await this.start(); + assert.containsOnce( + document.body, + '.o_MessagingMenu', + "should have messaging menu" + ); + + await afterNextRender(() => document.querySelector('.o_MessagingMenu_toggler').click()); + assert.containsOnce( + document.body, + '.o_MessagingMenu_tabButton[data-tab-id="all"]', + "should have a tab/filter 'all' in messaging menu" + ); + assert.containsOnce( + document.body, + '.o_MessagingMenu_tabButton[data-tab-id="chat"]', + "should have a tab/filter 'chat' in messaging menu" + ); + assert.hasClass( + document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="all"]'), + 'o-active', + "tab/filter 'all' of messaging menu should be active initially" + ); + assert.containsOnce( + document.body, + `.o_ThreadPreview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 11, + model: 'mail.channel', + }).localId + }"]`, + "livechat should be listed in 'all' tab/filter of messaging menu" + ); + + await afterNextRender(() => + document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]').click() + ); + assert.hasClass( + document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]'), + 'o-active', + "tab/filter 'chat' of messaging menu should become active after click" + ); + assert.containsOnce( + document.body, + `.o_ThreadPreview[data-thread-local-id="${ + this.env.models['mail.thread'].findFromIdentifyingData({ + id: 11, + model: 'mail.channel', + }).localId + }"]`, + "livechat should be listed in 'chat' tab/filter of messaging menu" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/im_livechat/static/src/models/thread/thread.js b/addons/im_livechat/static/src/models/thread/thread.js index 9f728f62d40..b3984500f8d 100644 --- a/addons/im_livechat/static/src/models/thread/thread.js +++ b/addons/im_livechat/static/src/models/thread/thread.js @@ -86,6 +86,12 @@ registerInstancePatchModel('mail.thread', 'im_livechat/static/src/models/thread/ } return this._super(); }, + /** + * @override + */ + _computeIsChatChannel() { + return this.channel_type === 'livechat' || this._super(); + }, }); }); diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index 59338e6dbc8..05fcf528240 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -33,6 +33,7 @@ + diff --git a/addons/mail/static/src/components/notification_list/notification_list.js b/addons/mail/static/src/components/notification_list/notification_list.js index a36bdaa34de..bcf43d651b1 100644 --- a/addons/mail/static/src/components/notification_list/notification_list.js +++ b/addons/mail/static/src/components/notification_list/notification_list.js @@ -187,7 +187,7 @@ class NotificationList extends Component { } else if (props.filter === 'chat') { return this.env.models['mail.thread'] .all(thread => - thread.channel_type === 'chat' && + thread.isChatChannel && thread.isPinned && thread.model === 'mail.channel' ) diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js index 09fcd46afed..bd40751ca59 100644 --- a/addons/mail/static/src/models/thread/thread.js +++ b/addons/mail/static/src/models/thread/thread.js @@ -964,6 +964,14 @@ function factory(dependencies) { return ['chat', 'livechat'].includes(this.channel_type); } + /** + * @private + * @returns {boolean} + */ + _computeIsChatChannel() { + return this.channel_type === 'chat'; + } + /** * @private * @returns {boolean} @@ -1426,6 +1434,19 @@ function factory(dependencies) { ], }), id: attr(), + /** + * States whether this thread is a `mail.channel` qualified as chat. + * + * Useful to list chat channels, like in messaging menu with the filter + * 'chat'. + */ + isChatChannel: attr({ + compute: '_computeIsChatChannel', + dependencies: [ + 'channel_type', + ], + default: false, + }), isCurrentPartnerFollowing: attr({ compute: '_computeIsCurrentPartnerFollowing', default: false, diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 78c6c6864c9..09196e9e5c3 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -507,6 +507,22 @@ var MockServer = Class.extend({ return Math.max(record.id, max); }, 0) + 1; }, + /** + * Simulate a 'call_button' operation from a view. + * + * @private + * @param {Object} param0 + * @param {Array} param0.args + * @param {Object} [param0.kargs] + * @param {string} param0.method + * @param {string} param0.model + * @returns {any} + * @throws {Error} in case the call button of provided model/method is not + * implemented. + */ + _mockCallButton({ args, kwargs, method, model }) { + throw new Error(`Unimplemented mocked call button on "${model}"/"${method}"`); + }, /** * Simulate a 'copy' operation, so we simply try to duplicate a record in * memory @@ -1820,6 +1836,8 @@ var MockServer = Class.extend({ */ _performRpc: function (route, args) { switch (route) { + case '/web/dataset/call_button': + return Promise.resolve(this._mockCallButton(args)); case '/web/action/load': return Promise.resolve(this._mockLoadAction(args)); diff --git a/addons/website_livechat/models/website_visitor.py b/addons/website_livechat/models/website_visitor.py index 21314c896b8..8a0017a7fed 100644 --- a/addons/website_livechat/models/website_visitor.py +++ b/addons/website_livechat/models/website_visitor.py @@ -57,8 +57,13 @@ class WebsiteVisitor(models.Model): operator = self.env.user country = visitor.country_id visitor_name = "%s (%s)" % (visitor.display_name, country.name) if country else visitor.display_name + channel_partner_to_add = [(4, operator.partner_id.id)] + if visitor.partner_id: + channel_partner_to_add.append((4, visitor.partner_id.id)) + else: + channel_partner_to_add.append((4, self.env.ref('base.public_partner').id)) mail_channel_vals_list.append({ - 'channel_partner_ids': [(4, operator.partner_id.id)], + 'channel_partner_ids': channel_partner_to_add, 'livechat_channel_id': visitor.website_id.channel_id.id, 'livechat_operator_id': self.env.user.partner_id.id, 'channel_type': 'livechat', @@ -73,6 +78,25 @@ class WebsiteVisitor(models.Model): if mail_channel_vals_list: mail_channels = self.env['mail.channel'].create(mail_channel_vals_list) # Open empty chatter to allow the operator to start chatting with the visitor. - mail_channels_info = mail_channels.channel_info('channel_minimize') + values = { + 'fold_state': 'open', + 'is_minimized': True, + } + mail_channels_uuid = mail_channels.mapped('uuid') + domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', 'in', mail_channels_uuid)] + channel_partners = self.env['mail.channel.partner'].search(domain) + channel_partners.write(values) + mail_channels_info = mail_channels.channel_info('send_chat_request') + notifications = [] for mail_channel_info in mail_channels_info: - self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', operator.partner_id.id), mail_channel_info) + notifications.append([(self._cr.dbname, 'res.partner', operator.partner_id.id), mail_channel_info]) + self.env['bus.bus'].sendmany(notifications) + + def _link_to_partner(self, partner, update_values=None): + """ Adapt partner in members of related livechats """ + if partner: + self.mail_channel_ids.channel_partner_ids = [ + (3, self.env.ref('base.public_partner').id), + (4, partner.id), + ] + super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values) diff --git a/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js new file mode 100644 index 00000000000..d43957ba374 --- /dev/null +++ b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js @@ -0,0 +1,32 @@ +odoo.define('website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js', function (require) { +'use strict'; + +const { registerInstancePatchModel } = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.messaging_notification_handler', 'website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js', { + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @override + */ + _handleNotificationPartner(data) { + const { info } = data; + if (info === 'send_chat_request') { + this._handleNotificationPartnerChannel(data); + const channel = this.env.models['mail.thread'].findFromIdentifyingData({ + id: data.id, + model: 'mail.channel', + }); + this.env.messaging.chatWindowManager.openThread(channel, { + makeActive: true, + }); + return; + } + return this._super(data); + }, +}); + +}); diff --git a/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js new file mode 100644 index 00000000000..4130af641b3 --- /dev/null +++ b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js @@ -0,0 +1,98 @@ +odoo.define('website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const FormView = require('web.FormView'); +const { + mock: { + intercept, + }, +} = require('web.test_utils'); + +QUnit.module('website_livechat', {}, function () { +QUnit.module('models', {}, function () { +QUnit.module('messaging_notification_handler', {}, function () { +QUnit.module('messaging_notification_handler_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, { + data: this.data, + }, params)); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('should open chat window on send chat request to website visitor', async function (assert) { + assert.expect(3); + + this.data['website.visitor'].records.push({ + id: 11, + name: "Visitor #11", + }); + await this.start({ + data: this.data, + hasChatWindow: true, + hasView: true, + // View params + View: FormView, + model: 'website.visitor', + arch: ` +
+
+
+ + + `, + res_id: 11, + }); + intercept(this.widget, 'execute_action', payload => { + this.env.services.rpc({ + route: '/web/dataset/call_button', + params: { + args: [payload.data.env.resIDs], + kwargs: { context: payload.data.env.context }, + method: payload.data.action_data.name, + model: payload.data.env.model, + } + }); + }); + + await afterNextRender(() => + document.querySelector('button[name="action_send_chat_request"]').click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window open after sending chat request to website visitor" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow'), + 'o-focused', + "chat window of livechat should be focused on open" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindowHeader_name').textContent, + "Visitor #11", + "chat window of livechat should have name of visitor in the name" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/website_livechat/static/tests/helpers/mock_models.js b/addons/website_livechat/static/tests/helpers/mock_models.js new file mode 100644 index 00000000000..93ae70331a8 --- /dev/null +++ b/addons/website_livechat/static/tests/helpers/mock_models.js @@ -0,0 +1,33 @@ +odoo.define('website_livechat/static/tests/helpers/mock_models.js', function (require) { +'use strict'; + +const MockModels = require('mail/static/tests/helpers/mock_models.js'); + +MockModels.patch('website_livechat/static/tests/helpers/mock_models.js', T => + class extends T { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + static generateData() { + const data = super.generateData(...arguments); + Object.assign(data, { + 'website.visitor': { + fields: {}, + records: [], + }, + }); + Object.assign(data['mail.channel'].fields, { + livechat_visitor_id: { string: "Visitor", type: 'many2one', relation: 'website.visitor' }, + }); + return data; + } + + } +); + +}); diff --git a/addons/website_livechat/static/tests/helpers/mock_server.js b/addons/website_livechat/static/tests/helpers/mock_server.js new file mode 100644 index 00000000000..8a75f07abfe --- /dev/null +++ b/addons/website_livechat/static/tests/helpers/mock_server.js @@ -0,0 +1,52 @@ +odoo.define('website_livechat/static/tests/helpers/mock_server.js', function (require) { +'use strict'; + +require('im_livechat/static/tests/helpers/mock_server.js'); // ensure mail overrides are applied first + +const MockServer = require('web.MockServer'); + +MockServer.include({ + /** + * Simulate a 'call_button' operation from a view. + * + * @override + */ + _mockCallButton({ args, kwargs, method, model }) { + if (model === 'website.visitor' && method === 'action_send_chat_request') { + return this._mockWebsiteVisitorActionSendChatRequest(args[0]); + } + return this._super(...arguments); + }, + /** + * @private + * @param {integer[]} ids + */ + _mockWebsiteVisitorActionSendChatRequest(ids) { + const visitors = this._getRecords('website.visitor', [['id', 'in', ids]]); + for (const visitor of visitors) { + const country = visitor.country_id + ? this._getRecords('res.country', [['id', '=', visitor.country_id]]) + : undefined; + const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`; + const members = [this.currentPartnerId]; + if (visitor.partner_id) { + members.push(visitor.partner_id); + } else { + members.push(this.publicPartnerId); + } + const livechatId = this._mockCreate('mail.channel', { + anonymous_name: visitor_name, + channel_type: 'livechat', + livechat_operator_id: this.currentPartnerId, + members, + public: 'private', + }); + // notify operator + const channelInfo = this._mockMailChannelChannelInfo([livechatId], 'send_chat_request')[0]; + const notification = [[false, 'res.partner', this.currentPartnerId], channelInfo]; + this._widget.call('bus_service', 'trigger', 'notification', [notification]); + } + }, +}); + +}); diff --git a/addons/website_livechat/views/assets.xml b/addons/website_livechat/views/assets.xml index 0311dbdf6e8..92f52b64f75 100644 --- a/addons/website_livechat/views/assets.xml +++ b/addons/website_livechat/views/assets.xml @@ -5,6 +5,7 @@