From 18426a62ebaa04b8c955a2b33b89d3feca8a6689 Mon Sep 17 00:00:00 2001 From: William Braeckman Date: Tue, 20 Sep 2022 11:49:41 +0000 Subject: [PATCH] [IMP] mail,hr: migrate m2x_avatar_user and m2x_avatar_employee to wowl2 Those fields may be used in the new views. Some of the legacy fields had to be kept because they are used in some legacy views, more notably the gantt and activity views as well as in the mrp work order tablet mode. X-original-commit: 1c871ad9ea4a77468911ff6a5546688a26398a8b Part-of: odoo/odoo#100764 --- addons/hr/__manifest__.py | 1 + .../hr/static/src/js/m2x_avatar_employee.js | 7 - .../many2many_avatar_employee_field.js | 15 ++ .../many2one_avatar_employee_field.js | 13 + addons/hr/static/src/views/open_chat_hook.js | 14 + .../tests/m2x_avatar_employee_legacy_tests.js | 182 +++++++++++++ .../static/tests/m2x_avatar_employee_tests.js | 30 +-- addons/mail/__manifest__.py | 4 +- addons/mail/static/src/js/m2x_avatar_user.js | 24 +- .../mail/static/src/scss/m2x_avatar_user.scss | 2 + .../views/fields/assign_user_command_hook.js | 117 ++++++++ .../many2many_avatar_user_field.js | 69 +++++ .../many2many_avatar_user_field.scss | 37 +++ .../many2many_avatar_user_field.xml | 15 ++ .../many2many_tags_email.js | 0 .../many2many_tags_email.xml | 0 .../many2one_avatar_user_field.js | 46 ++++ .../many2one_avatar_user_field.xml | 15 ++ .../mail/static/src/views/open_chat_hook.js | 27 ++ .../m2x_avatar_user_legacy_tests.js | 253 ++++++++++++++++++ .../m2x_avatar_user_tests.js | 70 ++--- 21 files changed, 860 insertions(+), 81 deletions(-) create mode 100644 addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js create mode 100644 addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js create mode 100644 addons/hr/static/src/views/open_chat_hook.js create mode 100644 addons/hr/static/tests/m2x_avatar_employee_legacy_tests.js create mode 100644 addons/mail/static/src/views/fields/assign_user_command_hook.js create mode 100644 addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.js create mode 100644 addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.scss create mode 100644 addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.xml rename addons/mail/static/src/{fields => views/fields/many2many_tags_email}/many2many_tags_email.js (100%) rename addons/mail/static/src/{fields => views/fields/many2many_tags_email}/many2many_tags_email.xml (100%) create mode 100644 addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js create mode 100644 addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.xml create mode 100644 addons/mail/static/src/views/open_chat_hook.js create mode 100644 addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index f170ee9c8fd..68782c6b4e4 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -66,6 +66,7 @@ 'web.qunit_suite_tests': [ 'hr/static/tests/helpers/*.js', 'hr/static/tests/m2x_avatar_employee_tests.js', + 'hr/static/tests/m2x_avatar_employee_legacy_tests.js', 'hr/static/tests/standalone_m2o_avatar_employee_tests.js', ], 'web.assets_tests': [ diff --git a/addons/hr/static/src/js/m2x_avatar_employee.js b/addons/hr/static/src/js/m2x_avatar_employee.js index 3c33366739c..80ce9811faa 100644 --- a/addons/hr/static/src/js/m2x_avatar_employee.js +++ b/addons/hr/static/src/js/m2x_avatar_employee.js @@ -4,9 +4,7 @@ import fieldRegistry from 'web.field_registry'; import { Many2OneAvatarUser, - KanbanMany2OneAvatarUser, KanbanMany2ManyAvatarUser, - ListMany2ManyAvatarUser, Many2ManyAvatarUser, } from '@mail/js/m2x_avatar_user'; @@ -44,10 +42,8 @@ const M2XAvatarEmployeeMixin = { }; 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); const M2MAvatarEmployeeMixin = Object.assign(M2XAvatarEmployeeMixin, { //---------------------------------------------------------------------- @@ -63,11 +59,8 @@ export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2MAvatarEmplo export const KanbanMany2ManyAvatarEmployee = KanbanMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {}); -export const ListMany2ManyAvatarEmployee = ListMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {}); - fieldRegistry.add('many2many_avatar_employee', Many2ManyAvatarEmployee); fieldRegistry.add('kanban.many2many_avatar_employee', KanbanMany2ManyAvatarEmployee); -fieldRegistry.add('list.many2many_avatar_employee', ListMany2ManyAvatarEmployee); export default { Many2OneAvatarEmployee, diff --git a/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js b/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js new file mode 100644 index 00000000000..6e3c519bc08 --- /dev/null +++ b/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Many2ManyTagsAvatarUserField, KanbanMany2ManyTagsAvatarUserField } from "@mail/views/fields/many2many_avatar_user_field/many2many_avatar_user_field"; + +export class Many2ManyTagsAvatarEmployeeField extends Many2ManyTagsAvatarUserField {} +Many2ManyTagsAvatarEmployeeField.additionalClasses = [...Many2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"]; + +registry.category("fields").add("many2many_avatar_employee", Many2ManyTagsAvatarEmployeeField); + +export class KanbanMany2ManyTagsAvatarEmployeeField extends KanbanMany2ManyTagsAvatarUserField {} +KanbanMany2ManyTagsAvatarEmployeeField.additionalClasses = [...KanbanMany2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"]; + +registry.category("fields").add("kanban.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField); +registry.category("fields").add("list.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField); diff --git a/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js b/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js new file mode 100644 index 00000000000..f05024ecbac --- /dev/null +++ b/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Many2OneAvatarUserField, KanbanMany2OneAvatarUserField } from "@mail/views/fields/many2one_avatar_user_field/many2one_avatar_user_field"; + +export class Many2OneAvatarEmployeeField extends Many2OneAvatarUserField {} +Many2OneAvatarEmployeeField.additionalClasses = [...Many2OneAvatarUserField.additionalClasses, "o_field_many2one_avatar_user"]; + +registry.category("fields").add("many2one_avatar_employee", Many2OneAvatarEmployeeField); + +export class KanbanMany2OneAvatarEmployeeField extends KanbanMany2OneAvatarUserField {} + +registry.category("fields").add("kanban.many2one_avatar_employee", KanbanMany2OneAvatarEmployeeField); diff --git a/addons/hr/static/src/views/open_chat_hook.js b/addons/hr/static/src/views/open_chat_hook.js new file mode 100644 index 00000000000..5178aacff60 --- /dev/null +++ b/addons/hr/static/src/views/open_chat_hook.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { helpers } from "@mail/views/open_chat_hook"; +import { patch } from "@web/core/utils/patch"; + +patch(helpers, "hr_m2x_avatar_employee", { + SUPPORTED_M2X_AVATAR_MODELS: [...helpers.SUPPORTED_M2X_AVATAR_MODELS, "hr.employee", "hr.employee.public"], + buildOpenChatParams: function (resModel, id) { + if (["hr.employee", "hr.employee.public"].includes(resModel)) { + return { employeeId: id }; + } + return this._super(...arguments); + }, +}); diff --git a/addons/hr/static/tests/m2x_avatar_employee_legacy_tests.js b/addons/hr/static/tests/m2x_avatar_employee_legacy_tests.js new file mode 100644 index 00000000000..5a72996a70b --- /dev/null +++ b/addons/hr/static/tests/m2x_avatar_employee_legacy_tests.js @@ -0,0 +1,182 @@ +/** @odoo-module **/ + +import { + start, + startServer, +} from '@mail/../tests/helpers/test_utils'; + +import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; + +import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee'; +import { dom } from 'web.test_utils'; + +QUnit.module('hr', {}, function () { + QUnit.module('M2XAvatarEmployeeLegacy', { + beforeEach() { + Many2OneAvatarEmployee.prototype.partnerIds = {}; + }, + }); + + QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ name: 'Mario' }); + const m2xHrAvatarUserId1 = pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1 }); + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ + mockRPC(route, args) { + if (args.method === 'read') { + assert.step(`read ${args.model} ${args.args[0]}`); + } + }, + serverData: { views }, + services: { + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with employees that have a dedicated user.", + "should display the correct information in the notification" + ); + }), + }, + }); + + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xHrAvatarUserId1, + views: [[false, 'form']], + }); + assert.strictEqual(document.querySelector('.o_field_widget[name=employee_id]').innerText.trim(), 'Mario'); + + await dom.click(document.querySelector('.o_m2o_avatar > img')); + + assert.verifySteps([ + `read m2x.avatar.employee ${m2xHrAvatarUserId1}`, + `read hr.employee.public ${hrEmployeePublicId1}`, + ]); + }); + + QUnit.test('many2many_avatar_employee widget in form view', async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{}, {}]); + const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]); + const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([ + { user_id: resUsersId1, user_partner_id: resPartnerId1 }, + { user_id: resUsersId2, user_partner_id: resPartnerId2 }, + ]); + const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create( + { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, + ); + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ + mockRPC(route, args) { + if (args.method === 'read') { + assert.step(`read ${args.model} ${args.args[0]}`); + } + }, + serverData: { views }, + }); + + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xAvatarEmployeeId1, + views: [[false, 'form']], + }); + assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + "should have 2 records"); + assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, + "should have correct avatar image"); + + await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); + await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]); + + assert.verifySteps([ + `read m2x.avatar.employee ${m2xAvatarEmployeeId1}`, + `read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`, + `read hr.employee.public ${hrEmployeePublicId1}`, + `read hr.employee.public ${hrEmployeePublicId2}`, + ]); + + assert.containsN( + document.body, + '.o_ChatWindowHeader_name', + 2, + "should have 2 chat windows" + ); + }); + + QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv['res.partner'].create({}); + const resUsersId1 = pyEnv['res.users'].create({}); + const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([ + {}, + { user_id: resUsersId1, user_partner_id: resPartnerId1 }, + ]); + const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create( + { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, + ); + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ + mockRPC(route, args) { + if (args.method === 'read') { + assert.step(`read ${args.model} ${args.args[0]}`); + } + }, + serverData: { views }, + services: { + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with employees that have a dedicated user.", + "should display the correct information in the notification" + ); + }), + }, + }); + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xAvatarEmployeeId1, + views: [[false, 'form']], + }); + + assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + "should have 2 records"); + assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, + "should have correct avatar image"); + + await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); + await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]); + + assert.verifySteps([ + `read m2x.avatar.employee ${hrEmployeePublicId1}`, + `read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`, + `read hr.employee.public ${hrEmployeePublicId1}`, + `read hr.employee.public ${hrEmployeePublicId2}` + ]); + + assert.containsOnce(document.body, '.o_ChatWindowHeader_name', + "should have 1 chat window"); + }); +}); diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index e702173d7e6..03c7c049346 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -55,9 +55,9 @@ QUnit.module('hr', {}, function () { views: [[false, 'list']], }); - assert.strictEqual(document.querySelector('.o_data_cell span').innerText, 'Mario'); - assert.strictEqual(document.querySelectorAll('.o_data_cell span')[1].innerText, 'Luigi'); - assert.strictEqual(document.querySelectorAll('.o_data_cell span')[2].innerText, 'Mario'); + assert.strictEqual(document.querySelector('.o_data_cell span:not(.o_m2o_avatar)').innerText, 'Mario'); + assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar)')[1].innerText, 'Luigi'); + assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar)')[2].innerText, 'Mario'); // click on first employee await afterNextRender(() => @@ -223,14 +223,14 @@ QUnit.module('hr', {}, function () { res_id: m2xAvatarEmployeeId1, views: [[false, 'form']], }); - assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2, "should have 2 records"); - assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'), `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, "should have correct avatar image"); - await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); - await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]); + await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')); + await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]); assert.verifySteps([ `read m2x.avatar.employee ${m2xAvatarEmployeeId1}`, @@ -279,7 +279,7 @@ QUnit.module('hr', {}, function () { res_model: 'm2x.avatar.employee', views: [[false, 'list']], }); - assert.containsN(document.body, '.o_data_cell:first .o_field_many2manytags > span', 2, + assert.containsN(document.body, '.o_data_cell:first .o_field_many2many_avatar_employee > div > span', 2, "should have two avatar"); // click on first employee badge @@ -366,12 +366,12 @@ QUnit.module('hr', {}, function () { res_model: 'm2x.avatar.employee', views: [[false, 'kanban']], }); - assert.containsN(document.body, '.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar', 2, + assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 2, "should have 2 avatar images"); - assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2manytags img.o_m2m_avatar').getAttribute('data-src'), + assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar').getAttribute('data-src'), `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, "should have correct avatar image"); - assert.strictEqual(document.querySelectorAll('.o_kanban_record .o_field_many2manytags img.o_m2m_avatar')[1].getAttribute('data-src'), + assert.strictEqual(document.querySelectorAll('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar')[1].getAttribute('data-src'), `/web/image/hr.employee.public/${hrEmployeePublicId2}/avatar_128`, "should have correct avatar image"); @@ -428,14 +428,14 @@ QUnit.module('hr', {}, function () { views: [[false, 'form']], }); - assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2, "should have 2 records"); - assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'), `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, "should have correct avatar image"); - await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); - await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]); + await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')); + await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]); assert.verifySteps([ `read m2x.avatar.employee ${hrEmployeePublicId1}`, diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index e4e5898d1e4..029f472964d 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -190,8 +190,6 @@ For more specific needs, you may also assign custom-defined actions # defines mixins and variables used by multiple components 'mail/static/src/components/notification_list/notification_list_item.scss', 'mail/static/src/js/**/*.js', - 'mail/static/src/fields/*.js', - 'mail/static/src/fields/*.xml', 'mail/static/src/utils/*.js', 'mail/static/src/scss/*.scss', 'mail/static/src/xml/*.xml', @@ -202,6 +200,8 @@ For more specific needs, you may also assign custom-defined actions ('include', 'mail.assets_messaging'), 'mail/static/src/services/*.js', 'mail/static/src/views/**/*.js', + 'mail/static/src/views/**/*.xml', + 'mail/static/src/views/**/*.scss', 'mail/static/src/webclient/commands/*.js', 'mail/static/src/widgets/*/*.js', 'mail/static/src/widgets/*/*.scss', diff --git a/addons/mail/static/src/js/m2x_avatar_user.js b/addons/mail/static/src/js/m2x_avatar_user.js index 500482240b0..c1502cf2e94 100644 --- a/addons/mail/static/src/js/m2x_avatar_user.js +++ b/addons/mail/static/src/js/m2x_avatar_user.js @@ -2,7 +2,7 @@ import core from 'web.core'; import fieldRegistry from 'web.field_registry'; -import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields'; +import { FieldMany2ManyTagsAvatar, Many2OneAvatar, KanbanMany2ManyTagsAvatar } from 'web.relational_fields'; import session from 'web.session'; const { Component } = owl; @@ -191,15 +191,6 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { } }); -export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({ - _template: 'mail.KanbanMany2OneAvatarUser', - - init() { - this._super(...arguments); - this.displayAvatarName = this.nodeOptions.display_avatar_name || false; - }, -}); - const M2MAvatarMixin = Object.assign(M2XAvatarMixin, { events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, { 'click .o_m2m_avatar': '_onAvatarClicked', @@ -327,13 +318,16 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi } }); +export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({ + _template: 'mail.KanbanMany2OneAvatarUser', + + init() { + this._super(...arguments); + this.displayAvatarName = this.nodeOptions.display_avatar_name || false; + }, +}); export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); -export const ListMany2ManyAvatarUser = ListMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); - fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser); -fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser); fieldRegistry.add('activity.many2one_avatar_user', KanbanMany2OneAvatarUser); fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser); -fieldRegistry.add('kanban.many2many_avatar_user', KanbanMany2ManyAvatarUser); -fieldRegistry.add('list.many2many_avatar_user', ListMany2ManyAvatarUser); diff --git a/addons/mail/static/src/scss/m2x_avatar_user.scss b/addons/mail/static/src/scss/m2x_avatar_user.scss index 5772dab986e..4f2d33bc8ac 100644 --- a/addons/mail/static/src/scss/m2x_avatar_user.scss +++ b/addons/mail/static/src/scss/m2x_avatar_user.scss @@ -3,12 +3,14 @@ filter: brightness(0.8); } +.o_field_many2one_avatar_user, .o_field_many2one_avatar.o_clickable_m2x_avatar { .o_m2o_avatar:hover { @include o-avatar-image-hover; } } +.o_field_many2many_avatar_user, .o_field_many2manytags.avatar.o_clickable_m2x_avatar { img.o_m2m_avatar { margin-right: 2px; diff --git a/addons/mail/static/src/views/fields/assign_user_command_hook.js b/addons/mail/static/src/views/fields/assign_user_command_hook.js new file mode 100644 index 00000000000..52177685622 --- /dev/null +++ b/addons/mail/static/src/views/fields/assign_user_command_hook.js @@ -0,0 +1,117 @@ +/** @odoo-module **/ + +import { useCommand } from "@web/core/commands/command_hook"; +import { useService } from "@web/core/utils/hooks"; +import { Domain } from "@web/core/domain"; + +const { useComponent, useEnv } = owl; + +/** + * Use this hook to add "Assign to.." and "Assign/Unassign me" to the command palette. + */ + +export function useAssignUserCommand() { + const component = useComponent(); + const env = useEnv(); + const orm = useService("orm"); + const user = useService("user"); + if ( + component.props.relation !== "res.users" || + component.props.record.activeFields[component.props.name].viewType !== "form" + ) { + return; + } + + const getCurrentIds = () => { + if (component.props.type === "many2one" && component.props.value) { + return [component.props.value[0]]; + } else if (component.props.type === "many2many") { + return component.props.value.currentIds; + } + return []; + }; + + const add = async (record) => { + if (component.props.type === "many2one") { + component.props.update(record); + } else if (component.props.type === "many2many") { + component.props.update({ + operation: "REPLACE_WITH", + resIds: [...getCurrentIds(), record[0]], + }); + } + }; + + const remove = async (record) => { + if (component.props.type === "many2one") { + component.props.update([]); + } else if (component.props.type === "many2many") { + component.props.update({ + operation: "REPLACE_WITH", + resIds: getCurrentIds().filter((id) => id !== record[0]), + }); + } + }; + + const provide = async (env, options) => { + const value = options.searchValue.trim(); + let domain = component.props.record.getFieldDomain(component.props.name); + const context = component.props.record.getFieldContext(component.props.name); + if (component.props.type === "many2many") { + const selectedUserIds = getCurrentIds(); + if (selectedUserIds.length) { + domain = Domain.and([domain, [["id", "not in", selectedUserIds]]]); + } + } + const searchResult = await orm.call(component.props.relation, "name_search", [], { + name: value, + args: domain.toList(), + operator: "ilike", + limit: 80, + context, + }); + return searchResult.map((record) => ({ + name: record[1], + action: add.bind(null, record), + })); + }; + + useCommand( + env._t("Assign to ..."), + () => ({ + configByNameSpace: { + default: { + emptyMessage: env._t("No users found"), + }, + }, + placeholder: env._t("Select a user..."), + providers: [ + { + provide, + }, + ], + }), + { + category: "smart_action", + hotkey: "alt+i", + global: true, + } + ); + + useCommand( + env._t("Assign/Unassign to me"), + () => { + const record = [user.userId, user.name]; + if (getCurrentIds().includes(user.userId)) { + remove(record); + } else { + add(record); + } + }, + { + category: "smart_action", + hotkey: "alt+shift+i", + global: true, + } + ); +} diff --git a/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.js b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.js new file mode 100644 index 00000000000..b68006e0a3f --- /dev/null +++ b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.js @@ -0,0 +1,69 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { TagsList } from "@web/views/fields/many2many_tags/tags_list"; +import { + Many2ManyTagsAvatarField, + ListKanbanMany2ManyTagsAvatarField, +} from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field"; +import { useOpenChat } from "@mail/views/open_chat_hook"; +import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook"; + +export class Many2ManyAvatarUserTagsList extends TagsList {} +Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList"; + +export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField { + setup() { + super.setup(); + this.openChat = useOpenChat(this.props.relation); + useAssignUserCommand(); + } + + get tags() { + return super.tags.map((tag) => ({ + ...tag, + onImageClicked: () => { + this.openChat(tag.resId); + }, + })); + } +} +Many2ManyTagsAvatarUserField.components = { + ...Many2ManyTagsAvatarField.components, + TagsList: Many2ManyAvatarUserTagsList, +}; +Many2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"]; + +registry.category("fields").add("many2many_avatar_user", Many2ManyTagsAvatarUserField); + +export class KanbanMany2ManyTagsAvatarUserField extends ListKanbanMany2ManyTagsAvatarField { + setup() { + super.setup(); + this.openChat = useOpenChat(this.props.relation); + useAssignUserCommand(); + } + + get displayText() { + const isList = this.props.record.activeFields[this.props.name].viewType === "list"; + return (isList && this.props.value.records.length === 1) || !this.props.readonly; + } + + get tags() { + const recordFromId = (id) => this.props.value.records.find((rec) => rec.id === id); + return super.tags.map((tag) => ({ + ...tag, + onImageClicked: () => { + this.openChat(recordFromId(tag.id).resId); + }, + })); + } +} +KanbanMany2ManyTagsAvatarUserField.template = "mail.KanbanMany2ManyTagsAvatarUserField"; +KanbanMany2ManyTagsAvatarUserField.components = { + ...ListKanbanMany2ManyTagsAvatarField.components, + TagsList: Many2ManyAvatarUserTagsList, +}; +KanbanMany2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"]; + +registry.category("fields").add("kanban.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField); +registry.category("fields").add("list.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField); diff --git a/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.scss b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.scss new file mode 100644 index 00000000000..a3af74599bc --- /dev/null +++ b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.scss @@ -0,0 +1,37 @@ +.o_kanban_renderer, .o_list_renderer { + .o_field_many2many_avatar_user { + &:not(:has(.o_tags_input)) { + display: flex !important; + } + + .o_field_tags:not(.o_tags_input) { + .o_tag { + background-color: transparent; + box-shadow: none; + color: inherit; + margin-right: 0px; + } + + .o_m2m_avatar, .o_m2m_avatar_empty { + width: 20px; + height: 20px; + margin-left: 0px; + } + + .o_m2m_avatar_empty { + background-color: $o-gray-300; + vertical-align: bottom; + } + } + } +} + +.o_kanban_renderer { + .o_field_many2many_avatar_user { + .o_field_tags { + .o_m2m_avatar, .o_m2m_avatar_empty { + margin-right: 0px; + } + } + } +} diff --git a/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.xml b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.xml new file mode 100644 index 00000000000..abfc51ac305 --- /dev/null +++ b/addons/mail/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field.xml @@ -0,0 +1,15 @@ + + + + + tag.onImageClicked + + + + + + !props.readonly + displayText + + + diff --git a/addons/mail/static/src/fields/many2many_tags_email.js b/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js similarity index 100% rename from addons/mail/static/src/fields/many2many_tags_email.js rename to addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js diff --git a/addons/mail/static/src/fields/many2many_tags_email.xml b/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.xml similarity index 100% rename from addons/mail/static/src/fields/many2many_tags_email.xml rename to addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.xml diff --git a/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js new file mode 100644 index 00000000000..ee9adce96a4 --- /dev/null +++ b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Many2OneAvatarField } from "@web/views/fields/many2one_avatar/many2one_avatar_field"; +import { useOpenChat } from "@mail/views/open_chat_hook"; +import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook"; + +export class Many2OneAvatarUserField extends Many2OneAvatarField { + setup() { + super.setup(); + this.openChat = useOpenChat(this.props.relation); + useAssignUserCommand(); + } + + onClickAvatar() { + this.openChat(this.props.value[0]); + } +} +Many2OneAvatarUserField.template = "mail.Many2OneAvatarUserField"; +Many2OneAvatarUserField.additionalClasses = ["o_field_many2one_avatar"]; + +registry.category("fields").add("many2one_avatar_user", Many2OneAvatarUserField); + +export class KanbanMany2OneAvatarUserField extends Many2OneAvatarUserField { + /** + * All props are normally passed to the Many2OneField however since + * we add a new one, we need to filter it out. + */ + get m2oFieldProps() { + return Object.fromEntries(Object.entries(this.props).filter(([key, _val]) => key in Many2OneAvatarField.props)); + } +} +KanbanMany2OneAvatarUserField.template = "mail.KanbanMany2OneAvatarUserField"; +KanbanMany2OneAvatarUserField.props = { + ...Many2OneAvatarUserField.props, + displayAvatarName: { type: Boolean, optional: true }, +}; +KanbanMany2OneAvatarUserField.extractProps = ({ attrs, field }) => { + return { + ...Many2OneAvatarUserField.extractProps({ attrs, field }), + displayAvatarName: attrs.options.display_avatar_name || false, + }; +}; + +registry.category("fields").add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUserField); +registry.category("fields").add("activity.many2one_avatar_user", KanbanMany2OneAvatarUserField); diff --git a/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.xml b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.xml new file mode 100644 index 00000000000..3a7c4db0b44 --- /dev/null +++ b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.xml @@ -0,0 +1,15 @@ + + + + + onClickAvatar + + + + + + props.readonly and props.displayAvatarName + m2oFieldProps + + + diff --git a/addons/mail/static/src/views/open_chat_hook.js b/addons/mail/static/src/views/open_chat_hook.js new file mode 100644 index 00000000000..cdba5a16075 --- /dev/null +++ b/addons/mail/static/src/views/open_chat_hook.js @@ -0,0 +1,27 @@ +/** @odoo-module **/ + +import { useService } from "@web/core/utils/hooks"; + +export const helpers = { + SUPPORTED_M2X_AVATAR_MODELS: ["res.users"], + buildOpenChatParams: (resModel, id) => { + if (resModel === "res.users") { + return { userId: id }; + } + }, +} + +export function useOpenChat(resModel) { + const messagingService = useService("messaging"); + if (!helpers.SUPPORTED_M2X_AVATAR_MODELS.includes(resModel)) { + throw new Error( + `This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify( + helpers.SUPPORTED_M2X_AVATAR_MODELS + )}` + ); + } + return async (id) => { + const messaging = await messagingService.get(); + messaging.openChat(helpers.buildOpenChatParams(resModel, id)); + }; +} diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js new file mode 100644 index 00000000000..b2b0090653b --- /dev/null +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js @@ -0,0 +1,253 @@ +/** @odoo-module **/ + +import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; +import { registry } from "@web/core/registry"; +import { makeLegacyCommandService } from "@web/legacy/utils"; +import core from 'web.core'; +import session from 'web.session'; +import makeTestEnvironment from "web.test_env"; +import { dom, nextTick } from 'web.test_utils'; + +let target; + +QUnit.module('mail', {}, function () { + QUnit.module('M2XAvatarUserLegacy', { + beforeEach() { + target = getFixture(); + }, + }); + + QUnit.test('many2many_avatar_user widget in form view', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); + const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', + res_id: m2xAvatarUserId1, + views: [[false, 'form']], + }); + + await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); + assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); + assert.strictEqual( + document.querySelector('.o_ChatWindowHeader_name').textContent, + 'Partner 1', + 'First chat window should be related to partner 1' + ); + }); + + QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1] = pyEnv['res.users'].create( + [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], + ); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'm2x.avatar.user', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + + triggerHotkey("control+k") + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") + assert.ok(idx >= 0); + + await click([...target.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Your Company, Mitchell Admin", + "Public user", + "Mario", + "Luigi", + "Yoshi", + ]) + await click(target, "#o_command_3") + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + }); + + QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'm2x.avatar.user', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + triggerHotkey("control+k") + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I") + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i") + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]) + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "") + }); + + QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv['res.users'].create( + [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }], + ); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'm2x.avatar.user', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k") + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") + assert.ok(idx >= 0); + + await click([...target.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Your Company, Mitchell Admin", + "Public user", + "Luigi" + ]); + + await click(target, "#o_command_2"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + }); + + QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create( + [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], + ); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'm2x.avatar.user', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + }); + + QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', + res_id: m2xAvatarUserId1, + views: [[false, 'form']], + }); + assert.strictEqual( + document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + `/web/image/res.users/${resUsersId1}/avatar_128`, + 'Should have correct avatar image' + ); + }); +}); diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js index 2e698c2a938..5826d05e1cd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js @@ -1,22 +1,18 @@ /** @odoo-module **/ -import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user'; import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; +import { click, getFixture, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; import { registry } from "@web/core/registry"; -import { makeLegacyCommandService } from "@web/legacy/utils"; -import core from 'web.core'; -import session from 'web.session'; -import makeTestEnvironment from "web.test_env"; +import { session } from "@web/session"; import { dom, nextTick } from 'web.test_utils'; +import { popoverService } from "@web/core/popover/popover_service"; +import { tooltipService } from "@web/core/tooltip/tooltip_service"; let target; QUnit.module('mail', {}, function () { QUnit.module('M2XAvatarUser', { beforeEach() { - // reset the cache before each test - Many2OneAvatarUser.prototype.partnerIds = {}; target = getFixture(); }, }); @@ -67,7 +63,7 @@ QUnit.module('mail', {}, function () { views: [[false, 'form']], }); - await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); + await dom.click(document.querySelector('.o_field_many2many_avatar_user .badge .o_m2m_avatar')); assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); assert.strictEqual( document.querySelector('.o_ChatWindowHeader_name').textContent, @@ -84,6 +80,8 @@ QUnit.module('mail', {}, function () { [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }, { name: "Tapu" }], ); pyEnv['m2x.avatar.user'].create({ user_ids: resUsersIds }); + registry.category("services").add("popover", popoverService); + registry.category("services").add("tooltip", tooltipService); const views = { 'm2x.avatar.user,false,kanban': ` @@ -111,17 +109,17 @@ QUnit.module('mail', {}, function () { views: [[false, 'kanban']], }); - assert.containsOnce(document.body, '.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty', + assert.containsOnce(document.body, '.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty', "should have o_m2m_avatar_empty span"); - assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty').innerText.trim(), "+2", + assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').innerText.trim(), "+2", "should have +2 in o_m2m_avatar_empty"); - document.querySelector('.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty').dispatchEvent(new Event('mouseover')); + document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').dispatchEvent(new Event('mouseenter')); await nextTick(); assert.containsOnce(document.body, '.popover', "should open a popover hover on o_m2m_avatar_empty"); - assert.strictEqual(document.querySelector('.popover .popover-body > div').innerText.trim(), 'Luigi', 'should have a right text in popover'); - assert.strictEqual(document.querySelectorAll('.popover .popover-body > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover'); + assert.strictEqual(document.querySelector('.popover .o-tooltip > div').innerText.trim(), 'Luigi', 'should have a right text in popover'); + assert.strictEqual(document.querySelectorAll('.popover .o-tooltip > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover'); }); QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { @@ -132,9 +130,6 @@ QUnit.module('mail', {}, function () { [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], ); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); const views = { 'm2x.avatar.user,false,form': '
', @@ -148,7 +143,7 @@ QUnit.module('mail', {}, function () { 'view_mode': 'form', 'views': [[false, 'form']], }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Mario") triggerHotkey("control+k") await nextTick(); @@ -165,8 +160,8 @@ QUnit.module('mail', {}, function () { "Yoshi", ]) await click(target, "#o_command_3") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + await nextTick(); + assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Luigi") }); QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { @@ -175,10 +170,7 @@ QUnit.module('mail', {}, function () { const pyEnv = await startServer(); const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); const views = { 'm2x.avatar.user,false,form': '
', @@ -192,23 +184,23 @@ QUnit.module('mail', {}, function () { 'view_mode': 'form', 'views': [[false, 'form']], }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Mario") triggerHotkey("control+k") await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I") + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I") assert.ok(idx >= 0); // Assign me (Luigi) triggerHotkey("alt+shift+i") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + await nextTick(); + assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user > div > a").textContent, "Luigi") // Unassign me triggerHotkey("control+k"); await nextTick(); await click([...target.querySelectorAll(".o_command")][idx]) - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "") + await nextTick(); + assert.containsNone(target, ".o_field_many2one_avatar_user > div > a"); }); QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { @@ -219,9 +211,6 @@ QUnit.module('mail', {}, function () { [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }], ); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); const views = { 'm2x.avatar.user,false,form': '
', @@ -252,7 +241,7 @@ QUnit.module('mail', {}, function () { ]); await click(target, "#o_command_2"); - await legacyExtraNextTick(); + await nextTick(); userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); }); @@ -265,10 +254,7 @@ QUnit.module('mail', {}, function () { [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], ); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); const views = { 'm2x.avatar.user,false,form': '
', @@ -287,12 +273,12 @@ QUnit.module('mail', {}, function () { triggerHotkey("control+k"); await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I"); + const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I"); assert.ok(idx >= 0); // Assign me (Luigi) triggerHotkey("alt+shift+i"); - await legacyExtraNextTick(); + await nextTick(); userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); @@ -300,7 +286,7 @@ QUnit.module('mail', {}, function () { triggerHotkey("control+k"); await nextTick(); await click([...target.querySelectorAll(".o_command")][idx]); - await legacyExtraNextTick(); + await nextTick(); userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); assert.deepEqual(userNames, ["Mario", "Yoshi"]); }); @@ -380,7 +366,7 @@ QUnit.module('mail', {}, function () { views: [[false, 'form']], }); assert.strictEqual( - document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), + document.querySelector('.o_field_many2many_avatar_user.o_field_widget .badge img').getAttribute('data-src'), `/web/image/res.users/${resUsersId1}/avatar_128`, 'Should have correct avatar image' );