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