[FIX] im_livechat, mail, website_livechat: open chat website visitor

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) <seb@odoo.com>
This commit is contained in:
Alexandre Kühn
2020-09-24 18:25:12 +00:00
parent f4c2eae24a
commit 7a0aecfca9
13 changed files with 394 additions and 10 deletions
@@ -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"
);
});
});
});
});
});
@@ -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();
},
});
});
@@ -33,6 +33,7 @@
<script type="text/javascript" src="/im_livechat/static/src/bugfix/bugfix_tests.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/components/composer/composer_tests.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/components/discuss/discuss_tests.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js"/>
<script type="text/javascript" src="/im_livechat/static/src/components/thread_icon/thread_icon_tests.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/legacy/public_livechat.js"></script>
@@ -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'
)
@@ -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,
@@ -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<integer[]>} 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));
@@ -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)
@@ -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);
},
});
});
@@ -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: `
<form>
<header>
<button name="action_send_chat_request" string="Send chat request" class="btn btn-primary" type="button"/>
</header>
<field name="name"/>
</form>
`,
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"
);
});
});
});
});
});
@@ -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;
}
}
);
});
@@ -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]);
}
},
});
});
+5 -1
View File
@@ -5,6 +5,7 @@
<template id="assets_backend" name="website livechat assets" inherit_id="web.assets_backend">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/website_livechat/static/src/bugfix/bugfix.js"></script>
<script type="text/javascript" src="/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js"/>
</xpath>
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_livechat/static/src/bugfix/bugfix.scss"/>
@@ -21,7 +22,10 @@
<template id="qunit_suite" name="website_livechat_tests" inherit_id="web.qunit_suite_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_livechat/static/src/bugfix/bugfix_tests.js"></script>
<script type="text/javascript" src="/website_livechat/static/src/bugfix/bugfix_tests.js"/>
<script type="text/javascript" src="/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js"/>
<script type="text/javascript" src="/website_livechat/static/tests/helpers/mock_models.js"/>
<script type="text/javascript" src="/website_livechat/static/tests/helpers/mock_server.js"/>
</xpath>
</template>
</data>
@@ -1,11 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="assets_backend" inherit_id="website.assets_backend" name="website_livechat assets backend">
<xpath expr="." position="inside">
</xpath>
</template>
<!--
Integrate Livechat in Common Frontend for Website
Template registering all the assets required to execute the Livechat from a page containing Odoo