From ffe768f28e9fb46ec443ee535a1f76ce59ac630c Mon Sep 17 00:00:00 2001 From: Mohammed Shekha Date: Mon, 22 Mar 2021 11:57:19 +0000 Subject: [PATCH] [IMP] hr: support many2many_avatar_employee widget closes odoo/odoo#67515 Related: odoo/enterprise#17081 Signed-off-by: Aaron Bohy (aab) --- addons/hr/__manifest__.py | 4 +- .../hr/static/src/js/m2x_avatar_employee.js | 57 ++++++ .../static/src/js/many2one_avatar_employee.js | 43 ----- ..._tests.js => m2x_avatar_employee_tests.js} | 173 +++++++++++++++++- 4 files changed, 229 insertions(+), 48 deletions(-) create mode 100644 addons/hr/static/src/js/m2x_avatar_employee.js delete mode 100644 addons/hr/static/src/js/many2one_avatar_employee.js rename addons/hr/static/tests/{many2one_avatar_employee_tests.js => m2x_avatar_employee_tests.js} (54%) diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index acdcee85c50..3d3ecd622b6 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -52,7 +52,7 @@ 'hr/static/src/scss/hr.scss', 'hr/static/src/js/chat.js', 'hr/static/src/js/language.js', - 'hr/static/src/js/many2one_avatar_employee.js', + 'hr/static/src/js/m2x_avatar_employee.js', 'hr/static/src/js/standalone_m2o_avatar_employee.js', 'hr/static/src/models/employee/employee.js', 'hr/static/src/models/messaging/messaging.js', @@ -61,7 +61,7 @@ ], 'web.qunit_suite_tests': [ 'hr/static/tests/helpers/mock_models.js', - 'hr/static/tests/many2one_avatar_employee_tests.js', + 'hr/static/tests/m2x_avatar_employee_tests.js', 'hr/static/tests/standalone_m2o_avatar_employee_tests.js', ], 'web.assets_qweb': [ diff --git a/addons/hr/static/src/js/m2x_avatar_employee.js b/addons/hr/static/src/js/m2x_avatar_employee.js new file mode 100644 index 00000000000..5e6961fab50 --- /dev/null +++ b/addons/hr/static/src/js/m2x_avatar_employee.js @@ -0,0 +1,57 @@ +/** @odoo-module alias=hr.Many2OneAvatarEmployee **/ + +import fieldRegistry from 'web.field_registry'; + +import { Many2OneAvatarUser, KanbanMany2OneAvatarUser } from '@mail/js/m2x_avatar_user'; +import { Many2ManyAvatarUser } from '@mail/js/m2x_avatar_user'; + + +// This module defines variants of the Many2OneAvatarUser and Many2ManyAvatarUser +// field widgets, to support fields pointing to 'hr.employee'. It also defines the +// kanban version of the Many2OneAvatarEmployee widget. +// +// Usage: +// + +const M2XAvatarEmployeeMixin = { + supportedModels: ['hr.employee', 'hr.employee.public'], + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + _getEmployeeID() { + return this.value.res_id; + }, + + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- + + /** + * @override + */ + _onAvatarClicked(ev) { + ev.stopPropagation(); // in list view, prevent from opening the record + const employeeId = this._getEmployeeID(ev); + this._openChat({ employeeId: employeeId }); + } +}; + +export const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(M2XAvatarEmployeeMixin); +export const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(M2XAvatarEmployeeMixin); + +fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee); +fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee); + +export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2XAvatarEmployeeMixin, { + _getEmployeeID(ev) { + return parseInt(ev.target.getAttribute('data-id'), 10); + }, +}); + +fieldRegistry.add('many2many_avatar_employee', Many2ManyAvatarEmployee); + +export default { + Many2OneAvatarEmployee, +}; diff --git a/addons/hr/static/src/js/many2one_avatar_employee.js b/addons/hr/static/src/js/many2one_avatar_employee.js deleted file mode 100644 index 8801b733c0a..00000000000 --- a/addons/hr/static/src/js/many2one_avatar_employee.js +++ /dev/null @@ -1,43 +0,0 @@ -odoo.define('hr.Many2OneAvatarEmployee', function (require) { - "use strict"; - - // This module defines a variant of the Many2OneAvatarUser field widget, - // to support many2one fields pointing to 'hr.employee'. It also defines the - // kanban version of this widget. - // - // Usage: - // - - const fieldRegistry = require('web.field_registry'); - const { Many2OneAvatarUser, KanbanMany2OneAvatarUser } = require('@mail/js/many2one_avatar_user'); - - const { Component } = owl; - - const Many2OneAvatarEmployeeMixin = { - supportedModels: ['hr.employee', 'hr.employee.public'], - - //---------------------------------------------------------------------- - // Private - //---------------------------------------------------------------------- - - /** - * @override - */ - 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); - const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(Many2OneAvatarEmployeeMixin); - - fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee); - fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee); - - return { - Many2OneAvatarEmployee, - KanbanMany2OneAvatarEmployee, - }; -}); diff --git a/addons/hr/static/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js similarity index 54% rename from addons/hr/static/tests/many2one_avatar_employee_tests.js rename to addons/hr/static/tests/m2x_avatar_employee_tests.js index f105077c47d..99bd0097d59 100644 --- a/addons/hr/static/tests/many2one_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -1,4 +1,4 @@ -odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) { +odoo.define('hr.M2XAvatarEmployeeTests', function (require) { "use strict"; const { @@ -15,7 +15,7 @@ const { Many2OneAvatarEmployee } = require('hr.Many2OneAvatarEmployee'); const { dom, mock } = require('web.test_utils'); QUnit.module('hr', {}, function () { - QUnit.module('Many2OneAvatarEmployee', { + QUnit.module('M2XAvatarEmployee', { beforeEach() { beforeEach(this); @@ -26,9 +26,10 @@ QUnit.module('hr', {}, function () { 'foo': { fields: { employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' }, + employee_ids: { string: "Employees", type: "many2many", relation: 'hr.employee.public' }, }, records: [ - { id: 1, employee_id: 11 }, + { id: 1, employee_id: 11, employee_ids: [11, 23] }, { id: 2, employee_id: 7 }, { id: 3, employee_id: 11 }, { id: 4, employee_id: 23 }, @@ -211,5 +212,171 @@ QUnit.module('hr', {}, function () { form.destroy(); }); + + QUnit.test('many2many_avatar_employee widget in form view', async function (assert) { + assert.expect(8); + + const { widget: form } = await start({ + hasChatWindow: true, + 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); + }, + res_id: 1, + }); + + assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + "should have 2 records"); + assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), + '/web/image/hr.employee.public/11/image_128', + "should have correct avatar image"); + + await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar')); + await dom.click(form.$('.o_field_many2manytags.avatar .badge:nth(1) .o_m2m_avatar')); + + assert.verifySteps([ + "read foo 1", + 'read hr.employee.public 11,23', + "read hr.employee.public 11", + "read hr.employee.public 23", + ]); + + assert.containsN( + document.body, + '.o_ChatWindowHeader_name', + 2, + "should have 2 chat windows" + ); + + form.destroy(); + }); + + QUnit.test('many2many_avatar_employee widget in list view', async function (assert) { + assert.expect(10); + + const { widget: list } = await start({ + hasChatWindow: true, + hasView: true, + View: ListView, + 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); + }, + }); + + assert.strictEqual(list.$('.o_data_cell:first .o_tag_badge_text').text().trim(), 'MarioYoshi'); + + // click on first employee badge + await afterNextRender(() => + dom.click(list.$('.o_data_cell:nth(0) .o_m2m_avatar:first')) + ); + assert.verifySteps( + ['read hr.employee.public 11,23', "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(0) .o_m2m_avatar:nth(1)') + )); + assert.verifySteps( + ['read hr.employee.public 23'], + "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, + "Yoshi", + 'chat window should be with clicked employee' + ); + + list.destroy(); + }); + + QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) { + assert.expect(10); + + 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({ + hasChatWindow: true, + 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); + }, + 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.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + "should have 2 records"); + assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), + '/web/image/hr.employee.public/11/image_128', + "should have correct avatar image"); + + await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar')); + await dom.click(form.$('.o_field_many2manytags.avatar .badge:nth(1) .o_m2m_avatar')); + + assert.verifySteps([ + 'read foo 1', + 'read hr.employee.public 11,23', + "read hr.employee.public 11", + "read hr.employee.public 23" + ]); + + 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" + ); + assert.containsOnce(document.body, '.o_ChatWindowHeader_name', + "should have 1 chat window"); + + form.destroy(); + }); }); });