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 @@ + +