diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py
index ada77010824..cb2942140ed 100644
--- a/addons/hr/__manifest__.py
+++ b/addons/hr/__manifest__.py
@@ -46,5 +46,8 @@
'installable': True,
'application': True,
'auto_install': False,
- 'qweb': ['static/src/xml/hr_templates.xml'],
+ 'qweb': [
+ 'static/src/bugfix/bugfix.xml',
+ 'static/src/xml/hr_templates.xml',
+ ],
}
diff --git a/addons/hr/static/src/bugfix/bugfix.js b/addons/hr/static/src/bugfix/bugfix.js
new file mode 100644
index 00000000000..0351046fab3
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.js
@@ -0,0 +1,10 @@
+/**
+ * This file allows introducing new JS modules without contaminating other files.
+ * This is useful when bug fixing requires adding such JS modules in stable
+ * versions of Odoo. Any module that is defined in this file should be isolated
+ * in its own file in master.
+ */
+odoo.define('hr/static/src/bugfix/bugfix.js', function (require) {
+'use strict';
+
+});
diff --git a/addons/hr/static/src/bugfix/bugfix.scss b/addons/hr/static/src/bugfix/bugfix.scss
new file mode 100644
index 00000000000..c4272e527b4
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.scss
@@ -0,0 +1,6 @@
+/**
+* This file allows introducing new styles without contaminating other files.
+* This is useful when bug fixing requires adding new components for instance in
+* stable versions of Odoo. Any style that is defined in this file should be isolated
+* in its own file in master.
+*/
diff --git a/addons/hr/static/src/bugfix/bugfix.xml b/addons/hr/static/src/bugfix/bugfix.xml
new file mode 100644
index 00000000000..c17906f75bb
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/addons/hr/static/src/bugfix/bugfix_tests.js b/addons/hr/static/src/bugfix/bugfix_tests.js
new file mode 100644
index 00000000000..8b23d37282d
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix_tests.js
@@ -0,0 +1,18 @@
+odoo.define('hr/static/src/bugfix/bugfix_tests.js', function (require) {
+'use strict';
+
+/**
+ * This file allows introducing new QUnit test modules without contaminating
+ * other test files. This is useful when bug fixing requires adding new
+ * components for instance in stable versions of Odoo. Any test that is defined
+ * in this file should be isolated in its own file in master.
+ */
+QUnit.module('hr', {}, function () {
+QUnit.module('bugfix', {}, function () {
+QUnit.module('bugfix_tests.js', {
+
+});
+});
+});
+
+});
diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js
index b9985e3929f..0489ccceac0 100644
--- a/addons/hr/static/src/js/chat.js
+++ b/addons/hr/static/src/js/chat.js
@@ -22,11 +22,7 @@ odoo.define('hr.employee_chat', function (require) {
var self = this;
return this._super.apply(this, arguments).then(function () {
var $chat_button = self.$el.find('.o_employee_chat_btn');
- if (self.state.context.uid === self.state.data.user_id.res_id) { // Hide the button for yourself
- $chat_button.hide();
- } else {
- $chat_button.off('click').on('click', self._onOpenChat.bind(self));
- }
+ $chat_button.off('click').on('click', self._onOpenChat.bind(self));
});
},
@@ -38,9 +34,8 @@ odoo.define('hr.employee_chat', function (require) {
_onOpenChat: function (ev) {
ev.preventDefault();
ev.stopImmediatePropagation();
- this.trigger_up('open_chat', {
- partner_id: this.state.data.user_partner_id.res_id
- });
+ const env = Component.env;
+ env.messaging.openChat({ employeeId: this.state.data.id });
return true;
},
};
@@ -48,23 +43,9 @@ odoo.define('hr.employee_chat', function (require) {
// USAGE OF CHAT MIXIN IN FORM VIEWS
var EmployeeFormRenderer = FormRenderer.extend(ChatMixin);
- var EmployeeFormController = FormController.extend({
- custom_events: _.extend({}, FormController.prototype.custom_events, {
- open_chat: '_onOpenChat'
- }),
-
- _onOpenChat: function (ev) {
- const env = Component.env;
- const partner = env.models['mail.partner'].insert({
- id: ev.data.partner_id,
- });
- partner.openChat();
- },
- });
-
var EmployeeFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
- Controller: EmployeeFormController,
+ Controller: FormController,
Renderer: EmployeeFormRenderer
}),
});
@@ -80,23 +61,9 @@ odoo.define('hr.employee_chat', function (require) {
}),
});
- var EmployeeKanbanController = KanbanController.extend({
- custom_events: _.extend({}, KanbanController.prototype.custom_events, {
- open_chat: '_onOpenChat'
- }),
-
- _onOpenChat: function (ev) {
- const env = Component.env;
- const partner = env.models['mail.partner'].insert({
- id: ev.data.partner_id,
- });
- partner.openChat();
- },
- });
-
var EmployeeKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
- Controller: EmployeeKanbanController,
+ Controller: KanbanController,
Renderer: EmployeeKanbanRenderer
}),
});
diff --git a/addons/hr/static/src/js/many2one_avatar_employee.js b/addons/hr/static/src/js/many2one_avatar_employee.js
index 5be29801ea9..0d5a245aaa2 100644
--- a/addons/hr/static/src/js/many2one_avatar_employee.js
+++ b/addons/hr/static/src/js/many2one_avatar_employee.js
@@ -8,48 +8,26 @@ odoo.define('hr.Many2OneAvatarEmployee', function (require) {
// Usage:
//
- const { _t } = require('web.core');
const fieldRegistry = require('web.field_registry');
const { Many2OneAvatarUser, KanbanMany2OneAvatarUser } = require('mail.Many2OneAvatarUser');
- const session = require('web.session');
+ const { Component } = owl;
const Many2OneAvatarEmployeeMixin = {
- supportedModel: 'hr.employee',
-
- /**
- * Set the field to read on 'hr.employee' to get the partner id.
- *
- * @override
- */
- init() {
- this._super(...arguments);
- this.partnerField = 'user_partner_id';
- },
+ supportedModels: ['hr.employee', 'hr.employee.public'],
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
- * Display a warning if the user clicked on himself, or on an employee
- * not associated with any user.
- *
* @override
- * @param {number} [partnerId] the id of the clicked partner
*/
- _displayWarning(partnerId) {
- if (partnerId !== session.partner_id) {
- // this is not ourself, so if we get here it means that the
- // employee is not associated with any user
- this.displayNotification({
- message: _t('You can only chat with employees that have a dedicated user'),
- type: 'info',
- });
- } else {
- this._super(...arguments);
- }
- },
+ async _onAvatarClicked(ev) {
+ ev.stopPropagation(); // in list view, prevent from opening the record
+ const env = Component.env;
+ await env.messaging.openChat({ employeeId: this.value.res_id });
+ }
};
const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(Many2OneAvatarEmployeeMixin);
diff --git a/addons/hr/static/src/models/employee/employee.js b/addons/hr/static/src/models/employee/employee.js
new file mode 100644
index 00000000000..0cf877baab6
--- /dev/null
+++ b/addons/hr/static/src/models/employee/employee.js
@@ -0,0 +1,204 @@
+odoo.define('hr/static/src/models/employee/employee.js', function (require) {
+'use strict';
+
+const { registerNewModel } = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+function factory(dependencies) {
+
+ class Employee extends dependencies['mail.model'] {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @static
+ * @param {Object} data
+ * @returns {Object}
+ */
+ static convertData(data) {
+ const data2 = {};
+ if ('id' in data) {
+ data2.id = data.id;
+ }
+ if ('user_id' in data) {
+ data2.hasCheckedUser = true;
+ if (!data.user_id) {
+ data2.user = [['unlink']];
+ } else {
+ const partnerNameGet = data['user_partner_id'];
+ const partnerData = {
+ display_name: partnerNameGet[1],
+ id: partnerNameGet[0],
+ };
+ const userNameGet = data['user_id'];
+ const userData = {
+ id: userNameGet[0],
+ partner: [['insert', partnerData]],
+ partnerDisplayName: userNameGet[1],
+ };
+ data2.user = [['insert', userData]];
+ }
+ }
+ return data2;
+ }
+
+ /**
+ * Performs the `read` RPC on the `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {string[]} param0.fields
+ * @param {integer[]} param0.ids
+ */
+ static async performRpcRead({ context, fields, ids }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'read',
+ args: [ids],
+ kwargs: {
+ context,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Performs the `search_read` RPC on `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {Array[]} param0.domain
+ * @param {string[]} param0.fields
+ */
+ static async performRpcSearchRead({ context, domain, fields }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'search_read',
+ kwargs: {
+ context,
+ domain,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Checks whether this employee has a related user and partner and links
+ * them if applicable.
+ */
+ async checkIsUser() {
+ return this.env.models['hr.employee'].performRpcRead({
+ ids: [this.id],
+ fields: ['user_id', 'user_partner_id'],
+ context: { active_test: false },
+ });
+ }
+
+ /**
+ * Gets the chat between the user of this employee and the current user.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @returns {mail.thread|undefined}
+ */
+ async getChat() {
+ if (!this.user && !this.hasCheckedUser) {
+ await this.async(() => this.checkIsUser());
+ }
+ // prevent chatting with non-users
+ if (!this.user) {
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only chat with employees that have a dedicated user."),
+ type: 'info',
+ });
+ return;
+ }
+ return this.user.getChat();
+ }
+
+ /**
+ * Opens a chat between the user of this employee and the current user
+ * and returns it.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @param {Object} [options] forwarded to @see `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(options) {
+ const chat = await this.async(() => this.getChat());
+ if (!chat) {
+ return;
+ }
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the most appropriate view that is a profile for this employee.
+ */
+ async openProfile() {
+ return this.env.messaging.openDocument({
+ id: this.id,
+ model: 'hr.employee.public',
+ });
+ }
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static _createRecordLocalId(data) {
+ return `${this.modelName}_${data.id}`;
+ }
+
+ }
+
+ Employee.fields = {
+ /**
+ * Whether an attempt was already made to fetch the user corresponding
+ * to this employee. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedUser: attr({
+ default: false,
+ }),
+ /**
+ * Unique identifier for this employee.
+ */
+ id: attr(),
+ /**
+ * Partner related to this employee.
+ */
+ partner: one2one('mail.partner', {
+ inverse: 'employee',
+ related: 'user.partner',
+ }),
+ /**
+ * User related to this employee.
+ */
+ user: one2one('mail.user', {
+ inverse: 'employee',
+ }),
+ };
+
+ Employee.modelName = 'hr.employee';
+
+ return Employee;
+}
+
+registerNewModel('hr.employee', factory);
+
+});
diff --git a/addons/hr/static/src/models/messaging/messaging.js b/addons/hr/static/src/models/messaging/messaging.js
new file mode 100644
index 00000000000..435761a91ef
--- /dev/null
+++ b/addons/hr/static/src/models/messaging/messaging.js
@@ -0,0 +1,36 @@
+odoo.define('hr/static/src/models/messaging/messaging.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+
+registerInstancePatchModel('mail.messaging', 'hr/static/src/models/messaging/messaging.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @param {integer} [param0.employeeId]
+ */
+ async getChat({ employeeId }) {
+ if (employeeId) {
+ const employee = this.env.models['hr.employee'].insert({ id: employeeId });
+ return employee.getChat();
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async openProfile({ id, model }) {
+ if (model === 'hr.employee' || model === 'hr.employee.public') {
+ const employee = this.env.models['hr.employee'].insert({ id });
+ return employee.openProfile();
+ }
+ return this._super(...arguments);
+ },
+});
+
+});
diff --git a/addons/hr/static/src/models/partner/partner.js b/addons/hr/static/src/models/partner/partner.js
new file mode 100644
index 00000000000..cbbcb987dd2
--- /dev/null
+++ b/addons/hr/static/src/models/partner/partner.js
@@ -0,0 +1,63 @@
+odoo.define('hr/static/src/models/partner/partner.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+registerInstancePatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Checks whether this partner has a related employee and links them if
+ * applicable.
+ */
+ async checkIsEmployee() {
+ await this.async(() => this.env.models['hr.employee'].performRpcSearchRead({
+ context: { active_test: false },
+ domain: [['user_partner_id', '=', this.id]],
+ fields: ['user_id', 'user_partner_id'],
+ }));
+ this.update({ hasCheckedEmployee: true });
+ },
+ /**
+ * When a partner is an employee, its employee profile contains more useful
+ * information to know who he is than its partner profile.
+ *
+ * @override
+ */
+ async openProfile() {
+ // limitation of patch, `this._super` becomes unavailable after `await`
+ const _super = this._super.bind(this, ...arguments);
+ if (!this.employee && !this.hasCheckedEmployee) {
+ await this.async(() => this.checkIsEmployee());
+ }
+ if (this.employee) {
+ return this.employee.openProfile();
+ }
+ return _super();
+ },
+});
+
+registerFieldPatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ /**
+ * Employee related to this partner. It is computed through
+ * the inverse relation and should be considered read-only.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'partner',
+ }),
+ /**
+ * Whether an attempt was already made to fetch the employee corresponding
+ * to this partner. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedEmployee: attr({
+ default: false,
+ }),
+});
+
+});
diff --git a/addons/hr/static/src/models/user/user.js b/addons/hr/static/src/models/user/user.js
new file mode 100644
index 00000000000..a0482d50deb
--- /dev/null
+++ b/addons/hr/static/src/models/user/user.js
@@ -0,0 +1,18 @@
+odoo.define('hr/static/src/models/user/user.js', function (require) {
+'use strict';
+
+const {
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { one2one } = require('mail/static/src/model/model_field.js');
+
+registerFieldPatchModel('mail.user', 'hr/static/src/models/user/user.js', {
+ /**
+ * Employee related to this user.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'user',
+ }),
+});
+
+});
diff --git a/addons/hr/static/tests/helpers/mock_models.js b/addons/hr/static/tests/helpers/mock_models.js
new file mode 100644
index 00000000000..d4ae7c86035
--- /dev/null
+++ b/addons/hr/static/tests/helpers/mock_models.js
@@ -0,0 +1,34 @@
+odoo.define('hr/static/tests/helpers/mock_models.js', function (require) {
+'use strict';
+
+const MockModels = require('mail/static/tests/helpers/mock_models.js');
+
+MockModels.patch('hr/static/tests/helpers/mock_models.js', T =>
+ class extends T {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static generateData() {
+ const data = super.generateData(...arguments);
+ Object.assign(data, {
+ 'hr.employee.public': {
+ fields: {
+ display_name: { string: "Name", type: "char" },
+ user_id: { string: "User", type: "many2one", relation: 'res.users' },
+ user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
+ },
+ records: [],
+ },
+ });
+ return data;
+ }
+
+ }
+);
+
+});
diff --git a/addons/hr/static/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/many2one_avatar_employee_tests.js
index 06ea5a74f07..c1eef47b182 100644
--- a/addons/hr/static/tests/many2one_avatar_employee_tests.js
+++ b/addons/hr/static/tests/many2one_avatar_employee_tests.js
@@ -1,7 +1,12 @@
odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) {
"use strict";
-const { afterEach, beforeEach, start } = require('mail/static/src/utils/test_utils.js');
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
const FormView = require('web.FormView');
const KanbanView = require('web.KanbanView');
@@ -20,7 +25,7 @@ QUnit.module('hr', {}, function () {
Object.assign(this.data, {
'foo': {
fields: {
- employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee' },
+ employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' },
},
records: [
{ id: 1, employee_id: 11 },
@@ -29,26 +34,17 @@ QUnit.module('hr', {}, function () {
{ id: 4, employee_id: 23 },
],
},
- 'hr.employee': {
- fields: {
- display_name: { string: "Name", type: "char" },
- user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
- },
- records: [{
- id: 11,
- name: "Mario",
- user_partner_id: 11,
- }, {
- id: 7,
- name: "Luigi",
- user_partner_id: 12,
- }, {
- id: 23,
- name: "Yoshi",
- user_partner_id: 13,
- }],
- },
});
+ this.data['hr.employee.public'].records.push(
+ { id: 11, name: "Mario", user_id: 11, user_partner_id: 11 },
+ { id: 7, name: "Luigi", user_id: 12, user_partner_id: 12 },
+ { id: 23, name: "Yoshi", user_id: 13, user_partner_id: 13 }
+ );
+ this.data['res.users'].records.push(
+ { id: 11, partner_id: 11 },
+ { id: 12, partner_id: 12 },
+ { id: 13, partner_id: 13 }
+ );
this.data['res.partner'].records.push(
{ id: 11, display_name: "Mario" },
{ id: 12, display_name: "Luigi" },
@@ -61,9 +57,10 @@ QUnit.module('hr', {}, function () {
});
QUnit.test('many2one_avatar_employee widget in list view', async function (assert) {
- assert.expect(4);
+ assert.expect(11);
const { widget: list } = await start({
+ hasChatWindow: true,
hasView: true,
View: ListView,
model: 'foo',
@@ -79,18 +76,59 @@ QUnit.module('hr', {}, function () {
assert.strictEqual(list.$('.o_data_cell span').text(), 'MarioLuigiMarioYoshi');
- await dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'));
- await dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar'));
- await dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'));
+ // click on first employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ ['read hr.employee.public 11'],
+ "first employee should have been read to find its partner"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 'should have opened chat window'
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatWindowHeader_name').textContent,
+ "Mario",
+ 'chat window should be with clicked employee'
+ );
+ // click on second employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar')
+ ));
+ assert.verifySteps(
+ ['read hr.employee.public 7'],
+ "second employee should have been read to find its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ 'should have opened second chat window'
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_ChatWindowHeader_name')[1].textContent,
+ "Luigi",
+ 'chat window should be with clicked employee'
+ );
- assert.verifySteps([
- 'read hr.employee 11',
- // 'call service openDMChatWindow 1',
- 'read hr.employee 7',
- // 'call service openDMChatWindow 2',
- // 'call service openDMChatWindow 1',
- ]);
+ // click on third employee (same as first)
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ [],
+ "employee should not have been read again because we already know its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ "should still have only 2 chat windows because third is the same partner as first"
+ );
list.destroy();
});
@@ -117,18 +155,19 @@ QUnit.module('hr', {}, function () {
assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), '');
assert.containsN(kanban, '.o_m2o_avatar', 4);
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0)').data('src'), '/web/image/hr.employee/11/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee/7/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee/11/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee/23/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee.public/7/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee.public/23/image_128');
kanban.destroy();
});
QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
- assert.expect(5);
+ assert.expect(6);
- this.data['hr.employee'].records[0].user_partner_id = false;
+ this.data['hr.employee.public'].records[0].user_id = false;
+ this.data['hr.employee.public'].records[0].user_partner_id = false;
const { widget: form } = await start({
hasView: true,
View: FormView,
@@ -156,50 +195,19 @@ QUnit.module('hr', {}, function () {
assert.verifySteps([
'read foo 1',
- 'read hr.employee 11',
- 'display notification "You can only chat with employees that have a dedicated user"',
+ 'read hr.employee.public 11',
]);
- form.destroy();
- });
-
- QUnit.test('many2one_avatar_employee: click on self', async function (assert) {
- assert.expect(5);
-
- this.data.currentPartnerId = 11;
- const { widget: form } = await start({
- hasView: true,
- View: FormView,
- model: 'foo',
- data: this.data,
- arch: '
',
- mockRPC(route, args) {
- if (args.method === 'read') {
- assert.step(`read ${args.model} ${args.args[0]}`);
- }
- return this._super(...arguments);
- },
- session: {
- partner_id: this.data.currentPartnerId,
- },
- res_id: 1,
- });
-
- mock.intercept(form, 'call_service', (ev) => {
- if (ev.data.service === 'notification') {
- assert.step(`display notification "${ev.data.args[0].message}"`);
- }
- }, true);
-
- assert.strictEqual(form.$('.o_field_widget[name=employee_id]').text().trim(), 'Mario');
-
- await dom.click(form.$('.o_m2o_avatar'));
-
- assert.verifySteps([
- 'read foo 1',
- 'read hr.employee 11',
- 'display notification "You cannot chat with yourself"',
- ]);
+ assert.containsOnce(
+ document.body,
+ '.toast .o_notification_content',
+ "should display a toast notification after failing to open chat"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "You can only chat with employees that have a dedicated user.",
+ "should display the correct information in the notification"
+ );
form.destroy();
});
diff --git a/addons/hr/views/hr_templates.xml b/addons/hr/views/hr_templates.xml
index d959b96506b..5d4bcd9da3e 100644
--- a/addons/hr/views/hr_templates.xml
+++ b/addons/hr/views/hr_templates.xml
@@ -4,15 +4,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
index dd9d08b606c..25e71660ebe 100644
--- a/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
+++ b/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -11,12 +11,9 @@ registerInstancePatchModel('mail.messaging_notification_handler', 'im_livechat/s
/**
* @override
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {integer} param0.partner_id
*/
- _handleNotificationChannelTypingStatus(data) {
- const { channelId, partner_id } = data;
+ _handleNotificationChannelTypingStatus(channelId, data) {
+ const { partner_id } = data;
const channel = this.env.models['mail.thread'].findFromIdentifyingData({
id: channelId,
model: 'mail.channel',
@@ -32,7 +29,7 @@ registerInstancePatchModel('mail.messaging_notification_handler', 'im_livechat/s
} else {
partnerId = partner_id;
}
- this._super(Object.assign(data, {
+ this._super(channelId, Object.assign(data, {
partner_id: partnerId,
}));
},
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js
index 4652da4975b..7f9b53d2670 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.js
+++ b/addons/mail/static/src/components/chat_window/chat_window.js
@@ -167,20 +167,15 @@ class ChatWindow extends Component {
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
- _onAutocompleteSelect(ev, ui) {
- const partnerId = ui.item.id;
- const partner = this.env.models['mail.partner'].find(partner => partner.id === partnerId);
- const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat');
- if (chat) {
- chat.open({ chatWindowMode: 'from_new_message' });
- } else {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- autoselectChatWindowMode: 'from_new_message',
- partnerId,
- type: 'chat',
- });
+ async _onAutocompleteSelect(ev, ui) {
+ const chat = await this.env.messaging.getChat({ partnerId: ui.item.id });
+ if (!chat) {
+ return;
}
+ this.env.messaging.chatWindowManager.openThread(chat, {
+ makeActive: true,
+ replaceNewMessage: true,
+ });
}
/**
@@ -225,6 +220,12 @@ class ChatWindow extends Component {
if (this.chatWindow.isFocused) {
return;
}
+ if (isEventHandled(ev, 'Message.authorOpenChat')) {
+ return;
+ }
+ if (isEventHandled(ev, 'Message.authorOpenProfile')) {
+ return;
+ }
this.chatWindow.focus();
}
diff --git a/addons/mail/static/src/components/chat_window/chat_window.xml b/addons/mail/static/src/components/chat_window/chat_window.xml
index d711f267ac7..f92a32cf2fe 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.xml
+++ b/addons/mail/static/src/components/chat_window/chat_window.xml
@@ -29,7 +29,6 @@
hasComposerCurrentPartnerAvatar="false"
hasComposerSendButton="env.messaging.device.isMobile"
hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
- haveMessagesAuthorRedirect="chatWindow.thread.channel_type !== 'chat'"
threadViewLocalId="chatWindow.threadView.localId"
t-on-focusin="_onFocusinThread"
t-ref="thread"
diff --git a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
index b598eeee4b4..c6ec0b74e9f 100644
--- a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
+++ b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
@@ -123,9 +123,7 @@ class ChatWindowHiddenMenu extends Component {
*/
_onClickedChatWindow(ev) {
const chatWindow = ev.detail.chatWindow;
- chatWindow.makeVisible();
- chatWindow.unfold();
- chatWindow.focus();
+ chatWindow.makeActive();
this.env.messaging.chatWindowManager.closeHiddenMenu();
}
diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
index 3306dbed905..efba64cecde 100644
--- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
+++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
@@ -255,6 +255,128 @@ QUnit.test('chat window new message: fold', async function (assert) {
);
});
+QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) {
+ /**
+ * InnerWith computation uses following info:
+ * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method)
+ * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual)
+ *
+ * - chat window width: 325px
+ * - start/end/between gap width: 10px/10px/5px
+ * - hidden menu width: 200px
+ * - global width: 1920px
+ *
+ * Enough space for 3 visible chat windows:
+ * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 1920
+ */
+ assert.expect(11);
+
+ this.data['res.partner'].records.push({ id: 131, name: "Partner 131" });
+ this.data['res.users'].records.push({ partner_id: 131 });
+ this.data['mail.channel'].records.push(
+ { is_minimized: true },
+ { is_minimized: true },
+ );
+ const imSearchDef = makeDeferred();
+ await this.start({
+ env: {
+ browser: {
+ innerWidth: 1920,
+ },
+ },
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (args.method === 'im_search') {
+ imSearchDef.resolve();
+ }
+ return res;
+ }
+ });
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 2,
+ "should have 2 chat windows initially"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should not have any 'new message' chat window initially"
+ );
+
+ // open "new message" chat window
+ await afterNextRender(() =>
+ document.querySelector(`.o_MessagingMenu_toggler`).click()
+ );
+ await afterNextRender(() =>
+ document.querySelector(`.o_MessagingMenu_newMessageButton`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should have 'new message' chat window after clicking 'new message' in messaging menu"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 3,
+ "should have 3 chat window after opening 'new message' chat window",
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindow_newMessageFormInput',
+ "'new message' chat window should have new message form input"
+ );
+ assert.hasClass(
+ document.querySelector('.o_ChatWindow[data-visible-index="2"]'),
+ 'o-new-message',
+ "'new message' chat window should be the last chat window initially",
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_ChatWindow[data-visible-index="2"] .o_ChatWindowHeader_commandShiftRight').click()
+ );
+ assert.hasClass(
+ document.querySelector('.o_ChatWindow[data-visible-index="1"]'),
+ 'o-new-message',
+ "'new message' chat window should have moved to the middle after clicking shift previous",
+ );
+
+ // search for a user in "new message" autocomplete
+ document.execCommand('insertText', false, "131");
+ document.querySelector(`.o_ChatWindow_newMessageFormInput`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ChatWindow_newMessageFormInput`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ // Wait for search RPC to be resolved. The following await lines are
+ // necessary because autocomplete is an external lib therefore it is not
+ // possible to use `afterNextRender`.
+ await imSearchDef;
+ await nextAnimationFrame();
+ const link = document.querySelector('.ui-autocomplete .ui-menu-item a');
+ assert.ok(
+ link,
+ "should have autocomplete suggestion after typing on 'new message' input"
+ );
+ assert.strictEqual(
+ link.textContent,
+ "Partner 131",
+ "autocomplete suggestion should target the partner matching search term"
+ );
+
+ await afterNextRender(() => link.click());
+ assert.containsNone(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should have removed the 'new message' chat window after selecting a partner"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeader_name').textContent,
+ "Partner 131",
+ "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle"
+ );
+});
+
QUnit.test('chat window: basic rendering', async function (assert) {
assert.expect(11);
@@ -1351,7 +1473,7 @@ QUnit.test('chat window: switch on TAB', async function (assert) {
);
});
-QUnit.test('chat window: TAB cycle with 3 open chat windows', async function (assert) {
+QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) {
/**
* InnerWith computation uses following info:
* ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method)
diff --git a/addons/mail/static/src/components/chatter/chatter.js b/addons/mail/static/src/components/chatter/chatter.js
index 61fbb78d619..cd1f8f8720b 100644
--- a/addons/mail/static/src/components/chatter/chatter.js
+++ b/addons/mail/static/src/components/chatter/chatter.js
@@ -37,19 +37,18 @@ class Chatter extends Component {
attachments: 1,
},
});
- this._threadRef = useRef('thread');
+ /**
+ * Reference of the composer. Useful to focus it.
+ */
+ this._composerRef = useRef('composer');
}
mounted() {
- if (this.chatter.thread) {
- this._notifyRendered();
- }
+ this._update();
}
patched() {
- if (this.chatter.thread) {
- this._notifyRendered();
- }
+ this._update();
}
//--------------------------------------------------------------------------
@@ -77,6 +76,22 @@ class Chatter extends Component {
});
}
+ /**
+ * @private
+ */
+ _update() {
+ if (this.chatter.thread) {
+ this._notifyRendered();
+ }
+ if (this.chatter.isDoFocus) {
+ this.chatter.update({ isDoFocus: false });
+ const composer = this._composerRef.comp;
+ if (composer) {
+ composer.focus();
+ }
+ }
+ }
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/components/chatter/chatter.xml b/addons/mail/static/src/components/chatter/chatter.xml
index b83c9ad733f..a48396f813c 100644
--- a/addons/mail/static/src/components/chatter/chatter.xml
+++ b/addons/mail/static/src/components/chatter/chatter.xml
@@ -39,10 +39,8 @@
class="o_Chatter_thread"
hasComposer="false"
hasScrollAdjust="chatter.hasMessageListScrollAdjust"
- haveMessagesAuthorRedirect="true"
order="'desc'"
threadViewLocalId="chatter.threadView.localId"
- t-ref="thread"
/>
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
index afb7d811eae..0b23ee4ec22 100644
--- a/addons/mail/static/src/components/composer/composer.js
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -121,7 +121,7 @@ class Composer extends Component {
if (this.env.messaging.device.isMobile) {
this.el.scrollIntoView();
}
- this.composer.focus();
+ this._textInputRef.comp.focus();
}
/**
@@ -288,7 +288,7 @@ class Composer extends Component {
ev.stopPropagation();
this._textInputRef.comp.saveStateInStore();
this.composer.insertIntoTextInput(ev.detail.unicode);
- this.composer.focus();
+ this.focus();
}
/**
@@ -323,7 +323,7 @@ class Composer extends Component {
if (ev.key === 'Escape') {
if (this._emojisPopoverRef.comp) {
this._emojisPopoverRef.comp.close();
- this.composer.focus();
+ this.focus();
markEventHandled(ev, 'Composer.closeEmojisPopover');
}
}
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
index 53616e112e9..6a0943f9ca8 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.js
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
@@ -82,6 +82,10 @@ class ComposerTextInput extends Component {
return this.env._t("Write something...");
}
+ focus() {
+ this._textareaRef.el.focus();
+ }
+
focusout() {
this.saveStateInStore();
this._textareaRef.el.blur();
@@ -155,10 +159,6 @@ class ComposerTextInput extends Component {
this.composer.textInputCursorEnd
);
this._updateHeight();
- if (this.composer.isDoFocus) {
- this._textareaRef.el.focus();
- this.composer.update({ isDoFocus: false });
- }
}
/**
@@ -273,7 +273,7 @@ class ComposerTextInput extends Component {
if (this.composer.activeSuggestedPartner) {
this.composer.insertMentionedPartner(this.composer.activeSuggestedPartner);
this.composer.closeMentionSuggestions();
- this.composer.focus();
+ this.focus();
}
}
break;
@@ -322,7 +322,7 @@ class ComposerTextInput extends Component {
_onPartnerMentionSuggestionClicked(ev) {
this.composer.insertMentionedPartner(ev.detail.partner);
this.composer.closeMentionSuggestions();
- this.composer.focus();
+ this.focus();
}
/**
diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js
index 83aee1e201e..4c8c4a58ab7 100644
--- a/addons/mail/static/src/components/discuss/discuss.js
+++ b/addons/mail/static/src/components/discuss/discuss.js
@@ -47,12 +47,13 @@ class Discuss extends Component {
});
this._updateLocalStoreProps();
/**
- * Reference of the thread. Useful to update scroll position correctly
- * on patch. AKU TODO: this made sense when composer was outside of
- * thread, but this may no longer be necessary??
+ * Reference of the composer. Useful to focus it.
*/
- this._threadRef = useRef('thread');
-
+ this._composerRef = useRef('composer');
+ /**
+ * Reference of the ThreadView. Useful to focus it.
+ */
+ this._threadViewRef = useRef('threadView');
// bind since passed as props
this._onMobileAddItemHeaderInputSelect = this._onMobileAddItemHeaderInputSelect.bind(this);
this._onMobileAddItemHeaderInputSource = this._onMobileAddItemHeaderInputSource.bind(this);
@@ -66,6 +67,7 @@ class Discuss extends Component {
this.discuss.openInitThread();
}
this._updateLocalStoreProps();
+ this._update();
}
patched() {
@@ -83,6 +85,7 @@ class Discuss extends Component {
}
this._activeThreadCache = this.discuss.threadView.threadCache;
this._updateLocalStoreProps();
+ this._update();
}
willUnmount() {
@@ -139,6 +142,24 @@ class Discuss extends Component {
// Private
//--------------------------------------------------------------------------
+ /**
+ * @private
+ */
+ _update() {
+ if (this.discuss.isDoFocus) {
+ this.discuss.update({ isDoFocus: false });
+ const composer = this._composerRef.comp;
+ if (composer) {
+ composer.focus();
+ } else {
+ const threadView = this._threadViewRef.comp;
+ if (threadView) {
+ threadView.focus();
+ }
+ }
+ }
+ }
+
/**
* @private
*/
@@ -248,16 +269,6 @@ class Discuss extends Component {
this.discuss.update({ activeMobileNavbarTabId: ev.detail.tabId });
}
- /**
- * @private
- * @param {CustomEvent} ev
- * @param {Object} ev.detail
- * @param {mail.thread} ev.detail.thread
- */
- _onSelectThread(ev) {
- this.discuss.threadView.update({ thread: [['link', ev.detail.thread]] });
- }
-
/**
* @private
* @param {CustomEvent} ev
diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml
index 94234d0fc9b..d88b4796d9c 100644
--- a/addons/mail/static/src/components/discuss/discuss.xml
+++ b/addons/mail/static/src/components/discuss/discuss.xml
@@ -58,7 +58,6 @@
hasComposerThreadTyping="true"
hasMessageCheckbox="true"
hasSquashCloseMessages="discuss.thread.model !== 'mail.box'"
- haveMessagesAuthorRedirect="discuss.thread.channel_type !== 'chat'"
haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox"
haveMessagesReplyIcon="discuss.thread === env.messaging.inbox"
selectedMessageLocalId="
@@ -66,6 +65,7 @@
discuss.replyingToMessage.localId"
threadViewLocalId="discuss.threadView.localId"
t-on-o-rendered="_onThreadRendered"
+ t-ref="threadView"
/>
@@ -77,7 +77,6 @@
@@ -96,6 +95,7 @@
hasDiscardButton="true"
hasThreadName="true"
t-on-o-message-posted="_onReplyingToMessageMessagePosted"
+ t-ref="composer"
/>
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
index 8c437ce5b18..3f9e93d1de8 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -2116,7 +2116,7 @@ QUnit.test('message origin redirect to channel', async function (assert) {
});
QUnit.test('redirect to author (open chat)', async function (assert) {
- assert.expect(9);
+ assert.expect(7);
// expected correspondent, with a random unique id that will be used to link
// partner to chat and a random name that will be asserted in the test
@@ -2143,13 +2143,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
id: 100,
model: 'mail.channel',
res_id: 1,
- },
- {
- author_id: this.data.currentPartnerId,
- channel_ids: [1],
- id: 101,
- model: 'mail.channel',
- res_id: 1,
}
);
await this.start({
@@ -2187,8 +2180,8 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
);
assert.strictEqual(
document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
- 2,
- "should have 2 messages"
+ 1,
+ "should have 1 message"
);
const msg1 = document.querySelector(`
.o_Discuss_thread
@@ -2196,12 +2189,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
this.env.models['mail.message'].find(message => message.id === 100).localId
}"]
`);
- const msg2 = document.querySelector(`
- .o_Discuss_thread
- .o_Message[data-message-local-id="${
- this.env.models['mail.message'].find(message => message.id === 101).localId
- }"]
- `);
assert.strictEqual(
msg1.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
1,
@@ -2211,15 +2198,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
msg1.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
"message1 should have redirect to author"
);
- assert.strictEqual(
- msg2.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
- 1,
- "message2 should have author image"
- );
- assert.notOk(
- msg2.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
- "message2 should not have redirect to author (self-author)"
- );
await afterNextRender(() =>
msg1.querySelector(`:scope .o_Message_authorAvatar`).click()
diff --git a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
index cea920a45f9..c2185a60dc0 100644
--- a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
+++ b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
@@ -74,7 +74,7 @@ class DiscussMobileMailboxSelection extends Component {
*/
_onClick(ev) {
const { mailbox } = ev.currentTarget.dataset;
- this.discuss.threadView.update({ thread: [['link', mailbox]] });
+ mailbox.open();
}
}
diff --git a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
index daaa8b44b5d..2b84762c9dd 100644
--- a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
+++ b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
@@ -117,7 +117,7 @@ class DiscussSidebarItem extends Component {
* @param {MouseEvent} ev
*/
_onClick(ev) {
- this.discuss.threadView.update({ thread: [['link', this.thread]] });
+ this.thread.open();
}
/**
diff --git a/addons/mail/static/src/components/follower/follower.js b/addons/mail/static/src/components/follower/follower.js
index f446c098914..dc3e395f03c 100644
--- a/addons/mail/static/src/components/follower/follower.js
+++ b/addons/mail/static/src/components/follower/follower.js
@@ -43,10 +43,7 @@ class Follower extends Component {
_onClickDetails(ev) {
ev.preventDefault();
ev.stopPropagation();
- this.env.messaging.openDocument({
- id: this.follower.resId,
- model: this.follower.resModel,
- });
+ this.follower.openProfile();
}
/**
diff --git a/addons/mail/static/src/components/follower/follower_tests.js b/addons/mail/static/src/components/follower/follower_tests.js
index d93cb24b6c2..a742e899e6b 100644
--- a/addons/mail/static/src/components/follower/follower_tests.js
+++ b/addons/mail/static/src/components/follower/follower_tests.js
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower/follower_tests.js', function (r
const components = {
Follower: require('mail/static/src/components/follower/follower.js'),
};
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
const {
afterEach,
afterNextRender,
@@ -141,8 +142,8 @@ QUnit.test('click on channel follower details', async function (assert) {
assert.step('do_action');
assert.strictEqual(
payload.action.res_id,
- 1,
- "The redirect action should redirect to the right res id (1)"
+ 10,
+ "The redirect action should redirect to the right res id (10)"
);
assert.strictEqual(
payload.action.res_model,
@@ -155,7 +156,8 @@ QUnit.test('click on channel follower details', async function (assert) {
"The redirect action should be of type 'ir.actions.act_window'"
);
});
-
+ this.data['res.partner'].records.push({ id: 100 });
+ this.data['mail.channel'].records.push({ id: 10 });
await this.start({
env: { bus },
});
@@ -164,7 +166,7 @@ QUnit.test('click on channel follower details', async function (assert) {
model: 'res.partner',
});
const follower = await this.env.models['mail.follower'].create({
- channel: [['insert', { id: 1, model: 'mail.channel', name: "channel" }]],
+ channel: [['insert', { id: 10, model: 'mail.channel', name: "channel" }]],
followedThread: [['link', thread]],
id: 2,
isActive: true,
@@ -192,6 +194,7 @@ QUnit.test('click on channel follower details', async function (assert) {
QUnit.test('click on partner follower details', async function (assert) {
assert.expect(7);
+ const openFormDef = makeDeferred();
const bus = new Bus();
bus.on('do-action', null, payload => {
assert.step('do_action');
@@ -210,8 +213,9 @@ QUnit.test('click on partner follower details', async function (assert) {
"ir.actions.act_window",
"The redirect action should be of type 'ir.actions.act_window'"
);
+ openFormDef.resolve();
});
-
+ this.data['res.partner'].records.push({ id: 100 });
await this.start({
env: { bus },
});
@@ -243,6 +247,7 @@ QUnit.test('click on partner follower details', async function (assert) {
);
document.querySelector('.o_Follower_details').click();
+ await openFormDef;
assert.verifySteps(
['do_action'],
"clicking on follower should redirect to partner form view"
@@ -302,6 +307,7 @@ QUnit.test('click on edit follower', async function (assert) {
QUnit.test('edit follower and close subtype dialog', async function (assert) {
assert.expect(6);
+ this.data['res.partner'].records.push({ id: 100 });
await this.start({
hasDialog: true,
async mockRPC(route, args) {
diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js
index 157f3dc2acd..7d314647bb2 100644
--- a/addons/mail/static/src/components/message/message.js
+++ b/addons/mail/static/src/components/message/message.js
@@ -11,6 +11,7 @@ const components = {
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const { markEventHandled } = require('mail/static/src/utils/utils.js');
const { timeFromNow } = require('mail.utils');
const { _lt } = require('web.core');
@@ -156,19 +157,18 @@ class Message extends Component {
}
/**
- * Determine whether author redirect feature is enabled on message.
- * Click on message author should redirect to author.
+ * Determines whether author open chat feature is enabled on message.
*
* @returns {boolean}
*/
- get hasAuthorRedirect() {
- if (!this.props.hasAuthorRedirect) {
- return false;
- }
+ get hasAuthorOpenChat() {
if (!this.message.author) {
return false;
}
- if (this.message.author === this.env.messaging.currentPartner) {
+ if (
+ this.message.originThread &&
+ this.message.originThread.correspondent === this.message.author
+ ) {
return false;
}
return true;
@@ -236,6 +236,13 @@ class Message extends Component {
return this.message.attachments.filter(attachment => attachment.fileType !== 'image');
}
+ /**
+ * @returns {string}
+ */
+ get OPEN_CHAT() {
+ return this.env._t("Open chat");
+ }
+
/**
* Make this message viewable in its enclosing scroll environment (usually
* message list).
@@ -245,7 +252,7 @@ class Message extends Component {
* @param {string} [param0.block='end']
* @returns {Promise}
*/
- async scrollIntoView({ behavior='auto', block='end' }={}) {
+ async scrollIntoView({ behavior = 'auto', block = 'end' } = {}) {
this.el.scrollIntoView({
behavior,
block,
@@ -419,23 +426,33 @@ class Message extends Component {
* @param {MouseEvent} ev
*/
_onClick(ev) {
+ if (ev.target.closest('.o_channel_redirect')) {
+ this.env.messaging.openProfile({
+ id: Number(ev.target.dataset.oeId),
+ model: 'mail.channel',
+ });
+ // avoid following dummy href
+ ev.preventDefault();
+ return;
+ }
if (ev.target.closest('.o_mention')) {
- this.env.messaging.redirect({
+ this.env.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
+ // avoid following dummy href
ev.preventDefault();
return;
}
if (ev.target.closest('.o_mail_redirect')) {
- this.env.messaging.redirect({
+ this.env.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
+ // avoid following dummy href
ev.preventDefault();
return;
}
- ev.stopPropagation();
this.state.isClicked = !this.state.isClicked;
}
@@ -443,17 +460,24 @@ class Message extends Component {
* @private
* @param {MouseEvent} ev
*/
- _onClickAuthor(ev) {
- if (!this.hasAuthorRedirect) {
+ _onClickAuthorAvatar(ev) {
+ if (!this.hasAuthorOpenChat) {
return;
}
+ markEventHandled(ev, 'Message.authorOpenChat');
+ this.message.author.openChat();
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickAuthorName(ev) {
if (!this.message.author) {
return;
}
- this.env.messaging.redirect({
- id: this.message.author.id,
- model: this.message.author.model,
- });
+ markEventHandled(ev, 'Message.authorOpenProfile');
+ this.message.author.openProfile();
}
/**
@@ -514,11 +538,9 @@ class Message extends Component {
* @param {MouseEvent} ev
*/
_onClickOriginThread(ev) {
+ // avoid following dummy href
ev.preventDefault();
- this.env.messaging.redirect({
- id: this.message.originThread.id,
- model: this.message.originThread.model,
- });
+ this.message.originThread.open();
}
/**
@@ -581,7 +603,6 @@ class Message extends Component {
Object.assign(Message, {
components,
defaultProps: {
- hasAuthorRedirect: false,
hasCheckbox: false,
hasMarkAsReadIcon: false,
hasReplyIcon: false,
@@ -594,7 +615,6 @@ Object.assign(Message, {
optional: true,
validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop),
},
- hasAuthorRedirect: Boolean,
hasCheckbox: Boolean,
hasMarkAsReadIcon: Boolean,
hasReplyIcon: Boolean,
diff --git a/addons/mail/static/src/components/message/message.xml b/addons/mail/static/src/components/message/message.xml
index 7739c79337a..b0a41947c88 100644
--- a/addons/mail/static/src/components/message/message.xml
+++ b/addons/mail/static/src/components/message/message.xml
@@ -18,7 +18,7 @@