diff --git a/addons/hr/static/src/js/m2x_avatar_employee.js b/addons/hr/static/src/js/m2x_avatar_employee.js index 5e6961fab50..3530b6de164 100644 --- a/addons/hr/static/src/js/m2x_avatar_employee.js +++ b/addons/hr/static/src/js/m2x_avatar_employee.js @@ -2,8 +2,9 @@ import fieldRegistry from 'web.field_registry'; -import { Many2OneAvatarUser, KanbanMany2OneAvatarUser } from '@mail/js/m2x_avatar_user'; +import { Many2OneAvatarUser, KanbanMany2OneAvatarUser, KanbanMany2ManyAvatarUser, ListMany2ManyAvatarUser } from '@mail/js/m2x_avatar_user'; import { Many2ManyAvatarUser } from '@mail/js/m2x_avatar_user'; +import { KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar } from 'web.relational_fields'; // This module defines variants of the Many2OneAvatarUser and Many2ManyAvatarUser @@ -44,13 +45,25 @@ export const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(M2XA fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee); fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee); -export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2XAvatarEmployeeMixin, { +const M2MAvatarEmployeeMixin = Object.assign(M2XAvatarEmployeeMixin, { + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + _getEmployeeID(ev) { return parseInt(ev.target.getAttribute('data-id'), 10); }, }); +export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {}); + +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/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index 8e943ed5f08..db7baf0cf00 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -280,7 +280,8 @@ QUnit.module('hr', {}, function () { }, }); - assert.strictEqual(list.$('.o_data_cell:first .o_tag_badge_text').text().trim(), 'MarioYoshi'); + assert.containsN(list, '.o_data_cell:first .o_field_many2manytags > span', 2, + "should have two avatar"); // click on first employee badge await afterNextRender(() => @@ -324,6 +325,59 @@ QUnit.module('hr', {}, function () { list.destroy(); }); + QUnit.test('many2many_avatar_employee widget in kanban view', async function (assert) { + assert.expect(7); + + const { widget: kanban } = await start({ + hasView: true, + View: KanbanView, + model: 'foo', + data: this.data, + arch: ` + + + +
+ +
+
+
+
`, + mockRPC(route, args) { + if (args.method === 'read') { + assert.step(`read ${args.model} ${args.args[0]}`); + } + return this._super(...arguments); + }, + }); + + assert.containsN(kanban, '.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar', 2, + "should have 2 avatar images"); + assert.strictEqual(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar:first').data('src'), + "/web/image/hr.employee.public/11/avatar_128", + "should have correct avatar image"); + assert.strictEqual(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar:eq(1)').data('src'), + "/web/image/hr.employee.public/23/avatar_128", + "should have correct avatar image"); + + await dom.click(kanban.$('.o_kanban_record:first .o_m2m_avatar:nth(0)')); + await dom.click(kanban.$('.o_kanban_record:first .o_m2m_avatar:nth(1)')); + + assert.verifySteps([ + "read hr.employee.public 11,23", + "read hr.employee.public 11", + "read hr.employee.public 23" + ]); + + kanban.destroy(); + }); + QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) { assert.expect(10); diff --git a/addons/mail/static/src/js/m2x_avatar_user.js b/addons/mail/static/src/js/m2x_avatar_user.js index c8f90b2feab..419a7b650f8 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 fieldRegistry from 'web.field_registry'; import { Many2OneAvatar } from 'web.relational_fields'; -import { FieldMany2ManyTagsAvatar } from 'web.relational_fields'; +import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar } from 'web.relational_fields'; const { Component } = owl; @@ -75,7 +75,7 @@ export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({ _template: 'mail.KanbanMany2OneAvatarUser', }); -export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2XAvatarMixin, { +const M2MAvatarMixin = Object.assign(M2XAvatarMixin, { events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, { 'click .o_m2m_avatar': '_onAvatarClicked', }), @@ -95,7 +95,15 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2XAvatarMixi }, }); +export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); + +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/tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/m2x_avatar_user_tests.js index 2ac5870beef..7e4f4350046 100644 --- a/addons/mail/static/tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/m2x_avatar_user_tests.js @@ -6,7 +6,7 @@ import FormView from 'web.FormView'; import KanbanView from 'web.KanbanView'; import ListView from 'web.ListView'; import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user'; -import { dom, mock } from 'web.test_utils'; +import { dom, mock, nextTick } from 'web.test_utils'; QUnit.module('mail', {}, function () { @@ -157,8 +157,35 @@ QUnit.module('mail', {}, function () { form.destroy(); }); + QUnit.test('many2many_avatar_user widget with single record in list view', async function (assert) { + assert.expect(4); + + this.data.foo.records[1].user_ids = [11]; + + const { widget: list } = await start({ + hasView: true, + View: ListView, + model: 'foo', + data: this.data, + arch: '', + res_id: 1, + }); + + assert.containsN(list, '.o_data_row:eq(0) .o_field_many2manytags.avatar.o_field_widget .o_m2m_avatar', 2, + "should have 2 records"); + assert.containsN(list, '.o_data_row:eq(1) .o_field_many2manytags.avatar.o_field_widget > div > span', 1, + "should have 1 record in second row"); + assert.containsN(list, '.o_data_row:eq(1) .o_field_many2manytags.avatar.o_field_widget > div > span', 1, + "should have img and span in second record"); + + await dom.click(list.$('.o_data_row:eq(1) .o_field_many2manytags.avatar:first > div > span')); + assert.containsOnce(list, '.o_selected_row'); + + list.destroy(); + }); + QUnit.test('many2many_avatar_user widget in list view', async function (assert) { - assert.expect(7); + assert.expect(8); const { widget: list } = await start({ hasView: true, @@ -178,9 +205,14 @@ QUnit.module('mail', {}, function () { assert.step('open record'); }); - assert.containsN(list.$(".o_data_cell:first"), '.o_field_many2manytags.avatar.o_field_widget .badge', 2, + assert.containsN(list.$(".o_data_cell:first"), '.o_field_many2manytags.avatar.o_field_widget span', 2, "should have 2 records"); - assert.strictEqual(list.$('.o_data_cell:first .o_tag_badge_text').text().trim(), 'MarioYoshi'); + assert.strictEqual(list.$(".o_data_cell:first .o_field_many2manytags.avatar img.o_m2m_avatar:first").data('src'), + "/web/image/res.users/11/avatar_128", + "should have right image"); + assert.strictEqual(list.$(".o_data_cell:eq(0) .o_field_many2manytags.avatar img.o_m2m_avatar:eq(1)").data('src'), + "/web/image/res.users/23/avatar_128", + "should have right image"); // sanity check: later on, we'll check that clicking on the avatar doesn't open the record await dom.click(list.$('.o_data_row:first .o_field_many2manytags')); @@ -198,4 +230,78 @@ QUnit.module('mail', {}, function () { list.destroy(); }); + QUnit.test('many2many_avatar_user in kanban view', async function (assert) { + assert.expect(11); + + this.data['res.users'].records.push({ id: 15, name: "Tapu", partner_id: 11 },); + this.data.foo.records[2].user_ids = [11, 23, 7, 15]; + + const { widget: kanban } = await start({ + hasView: true, + View: KanbanView, + model: 'foo', + data: this.data, + arch: ` + + + +
+ + +
+
+
+
`, + mockRPC(route, args) { + if (args.method === 'read') { + assert.step(`read ${args.model} ${args.args[0]}`); + } + return this._super(...arguments); + }, + }); + + mock.intercept(kanban, 'open_record', () => { + assert.step('open record'); + }); + + assert.strictEqual(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar:first').data('src'), + "/web/image/res.users/11/avatar_128", + "should have correct avatar image"); + assert.strictEqual(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar:eq(1)').data('src'), + "/web/image/res.users/23/avatar_128", + "should have correct avatar image"); + + assert.containsN(kanban, '.o_kanban_record:eq(2) .o_field_many2manytags > span:not(.o_m2m_avatar_empty)', 2, + "should have 2 records"); + assert.strictEqual(kanban.$('.o_kanban_record:eq(2) .o_field_many2manytags img.o_m2m_avatar:first').data('src'), + "/web/image/res.users/11/avatar_128", + "should have correct avatar image"); + assert.strictEqual(kanban.$('.o_kanban_record:eq(2) .o_field_many2manytags img.o_m2m_avatar:eq(1)').data('src'), + "/web/image/res.users/23/avatar_128", + "should have correct avatar image"); + assert.containsOnce(kanban, '.o_kanban_record:eq(2) .o_field_many2manytags .o_m2m_avatar_empty', + "should have o_m2m_avatar_empty span"); + assert.strictEqual(kanban.$('.o_kanban_record:eq(2) .o_field_many2manytags .o_m2m_avatar_empty').text().trim(), "+2", + "should have +2 in o_m2m_avatar_empty"); + + kanban.$('.o_kanban_record:eq(2) .o_field_many2manytags .o_m2m_avatar_empty').trigger($.Event('mouseenter')); + await nextTick(); + assert.containsOnce(kanban, '.popover', + "should open a popover hover on o_m2m_avatar_empty"); + assert.strictEqual(kanban.$('.popover .popover-body > div').text().trim(), "LuigiTapu", + "should have a right text in popover"); + + assert.verifySteps([ + "read res.users 7,11,15,23", + ]); + + kanban.destroy(); + }); + }); diff --git a/addons/web/static/src/legacy/js/fields/field_registry.js b/addons/web/static/src/legacy/js/fields/field_registry.js index 5331f6feb00..d8b23ca60eb 100644 --- a/addons/web/static/src/legacy/js/fields/field_registry.js +++ b/addons/web/static/src/legacy/js/fields/field_registry.js @@ -87,6 +87,8 @@ registry .add('many2many_binary', relational_fields.FieldMany2ManyBinaryMultiFiles) .add('many2many_tags', relational_fields.FieldMany2ManyTags) .add('many2many_tags_avatar', relational_fields.FieldMany2ManyTagsAvatar) + .add('kanban.many2many_tags_avatar', relational_fields.KanbanMany2ManyTagsAvatar) + .add('list.many2many_tags_avatar', relational_fields.ListMany2ManyTagsAvatar) .add('form.many2many_tags', relational_fields.FormFieldMany2ManyTags) .add('kanban.many2many_tags', relational_fields.KanbanFieldMany2ManyTags) .add('many2many_checkboxes', relational_fields.FieldMany2ManyCheckBoxes) diff --git a/addons/web/static/src/legacy/js/fields/relational_fields.js b/addons/web/static/src/legacy/js/fields/relational_fields.js index 0bdda5eedbe..ccce78a9c9a 100644 --- a/addons/web/static/src/legacy/js/fields/relational_fields.js +++ b/addons/web/static/src/legacy/js/fields/relational_fields.js @@ -2772,6 +2772,72 @@ var FieldMany2ManyTagsAvatar = FieldMany2ManyTags.extend({ }, }); + +// Remove event handlers on this widget to ensure that the kanban 'global +// click' opens the clicked record +const { click, ...M2MAvatarMixinEvents } = AbstractField.prototype.events; +const M2MAvatarMixin = { + visibleAvatarCount: 3, // number of visible avatar + events: M2MAvatarMixinEvents, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Open tooltip on empty avatar clicked + * + * @private + */ + _bindPopover(ev) { + this.$('.o_m2m_avatar_empty').popover({ + container: this.$el, + trigger: 'hover', + html: true, + placement: 'auto', + content: () => { + const elements = this.value ? _.pluck(this.value.data, 'data') : []; + return qweb.render('Many2ManyTagAvatarPopover', { + elements: elements.slice(this.visibleAvatarCount - 1), + }); + }, + }); + }, + /** + * @override + */ + _getRenderTagsContext() { + const result = this._super(...arguments); + result['widget'] = this; + return result; + }, + /** + * @override + */ + _renderReadonly() { + this.$el.addClass('o_field_many2manytags_multi'); + return this._super(...arguments); + }, + /** + * Override to bind popover + * + * @override + */ + _renderTags() { + this._super(...arguments); + this._bindPopover(); + }, +} + +const KanbanMany2ManyTagsAvatar = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, { + tag_template: 'KanbanMany2ManyTagAvatar', +}); + +const ListMany2ManyTagsAvatar = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, { + tag_template: 'ListMany2ManyTagAvatar', + visibleAvatarCount: 5, +}); + var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({ events: _.extend({}, FieldMany2ManyTags.prototype.events, { 'click .dropdown-toggle': '_onOpenColorPicker', @@ -3697,6 +3763,8 @@ return { FieldMany2ManyCheckBoxes: FieldMany2ManyCheckBoxes, FieldMany2ManyTags: FieldMany2ManyTags, FieldMany2ManyTagsAvatar: FieldMany2ManyTagsAvatar, + KanbanMany2ManyTagsAvatar: KanbanMany2ManyTagsAvatar, + ListMany2ManyTagsAvatar: ListMany2ManyTagsAvatar, FormFieldMany2ManyTags: FormFieldMany2ManyTags, KanbanFieldMany2ManyTags: KanbanFieldMany2ManyTags, diff --git a/addons/web/static/src/legacy/scss/fields.scss b/addons/web/static/src/legacy/scss/fields.scss index 022c35e8513..bc39b966d57 100644 --- a/addons/web/static/src/legacy/scss/fields.scss +++ b/addons/web/static/src/legacy/scss/fields.scss @@ -296,6 +296,27 @@ } } } + &.o_field_many2manytags_multi { + margin-bottom: 0px; + > span { + display: inline-block; + margin-right: 2px; + } + .o_m2m_avatar, .o_m2m_avatar_empty { + width: 20px; + height: 20px; + margin-left: 0px; + } + .o_m2m_avatar_empty { + background-color: #dee2e6; + vertical-align: bottom; + } + &.avatar.o_clickable_m2x_avatar { + img.o_m2m_avatar { + margin-right: 0px; + } + } + } } // Stars diff --git a/addons/web/static/src/legacy/scss/kanban_view.scss b/addons/web/static/src/legacy/scss/kanban_view.scss index b49c3f6ad18..1f0acbd27a9 100644 --- a/addons/web/static/src/legacy/scss/kanban_view.scss +++ b/addons/web/static/src/legacy/scss/kanban_view.scss @@ -267,6 +267,14 @@ line-height: 1.2; word-break: break-all; + &.avatar { + margin: 0 6px; + .o_m2m_avatar_empty > span { + display: block; + margin-top: 3px; + } + } + .o_tag { display: inline-block; margin-right: 4px; diff --git a/addons/web/static/src/legacy/scss/list_view.scss b/addons/web/static/src/legacy/scss/list_view.scss index 857e0cbd80b..3616a32da68 100644 --- a/addons/web/static/src/legacy/scss/list_view.scss +++ b/addons/web/static/src/legacy/scss/list_view.scss @@ -244,6 +244,13 @@ pointer-events: auto; } } + &.o_many2many_avatar_user_cell { + .o_field_many2manytags { + > span { + margin-right: 2px; + } + } + } } .o_data_row:not(.o_selected_row) { diff --git a/addons/web/static/src/legacy/xml/base.xml b/addons/web/static/src/legacy/xml/base.xml index f9b91f8f3cd..329fdfa66e7 100644 --- a/addons/web/static/src/legacy/xml/base.xml +++ b/addons/web/static/src/legacy/xml/base.xml @@ -1336,6 +1336,48 @@ + + + + + + + + 9+ + + + + + + + + + + + +
+ + +
+
+ + + + + + + + 9+ + + + + + + +
+ + +
+
+