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' );