[IMP] mail: support many2many_avatar in kanban and list view

Purpose
The aim of the current task is to adapt the design of this new widget to the
list and kanban views.

SPECIFICATION

Kanban
when there is only one record set in the m2m field, display it like a
many2one_avatar widget when there are two or three records set in the m2m
field, display the avatars next to each other when there are more than three
records set in the m2m field, display the first two avatars, then a grey circle
with +X in it (where X is the number of records beyond the first two), when
hovering the +X avatar, open a tooltip with the list of the remaining records
the display order of the records is the same as the order in the m2m field
the avatars behave like in the other avatar widgets (i.e. darkens on hover and
clicking on it opens the chat) except the +X avatar that does not darken or has
a cursor:pointer; on hover and is not clickable

List
same specs as for the kanban view, with the difference that the widget displays
up to five records instead of three when the widget is editable display it like
the current formview version of the many2many_avatar_user widget (i.e. tags
with avatars)

task-2563591

closes odoo/odoo#72166

Signed-off-by: Simon Genin (ges@odoo) <ges@odoo.com>
This commit is contained in:
Mohammed Shekha
2021-08-06 09:12:08 +00:00
parent a96757e19c
commit 01c460f37a
11 changed files with 522 additions and 9 deletions
+15 -2
View File
@@ -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,
@@ -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: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="employee_ids" widget="many2many_avatar_employee"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
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);
+10 -2
View File
@@ -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);
@@ -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: '<tree editable="top"><field name="user_ids" widget="many2many_avatar_user"/></tree>',
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: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="user_id"/>
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_ids" widget="many2many_avatar_user"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
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();
});
});
@@ -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)
@@ -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,
@@ -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
@@ -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;
@@ -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) {
+42
View File
@@ -1336,6 +1336,48 @@
<span><img t-attf-src="/web/image/#{avatarModel}/#{el.id}/#{avatarField}" t-att-data-id="el.id" class="rounded-circle o_m2m_avatar"/></span>
</t>
</t>
<t t-name="KanbanMany2ManyTagAvatar">
<t t-set="visibleElements" t-value="elements.length === widget.visibleAvatarCount ? elements : elements.slice(0, widget.visibleAvatarCount - 1)"/>
<t t-foreach="visibleElements" t-as="el">
<span><img t-attf-src="/web/image/#{avatarModel}/#{el.id}/#{avatarField}" t-att-title="el.display_name" t-att-data-id="el.id" class="rounded-circle o_m2m_avatar"/></span>
</t>
<t t-if="elements.length > widget.visibleAvatarCount">
<span class="o_m2m_avatar_empty rounded-circle text-center font-weight-bold">
<span t-if="elements.length - widget.visibleAvatarCount + 1 > 9">9+</span>
<span t-else="">+<t t-esc="elements.length - widget.visibleAvatarCount + 1"/></span>
</span>
</t>
</t>
<t t-name="ListMany2ManyTagAvatar">
<t t-if="widget.mode !== 'readonly'">
<t t-call="FieldMany2ManyTagAvatar"/>
</t>
<t t-else="">
<t t-if="elements.length === 1">
<div>
<img t-attf-src="/web/image/#{avatarModel}/#{elements[0].id}/#{avatarField}" t-att-data-id="elements[0].id" class="rounded-circle o_m2m_avatar"/>
<span t-esc="elements[0].display_name"/>
</div>
</t>
<t t-else="">
<t t-set="visibleElements" t-value="elements.length === widget.visibleAvatarCount ? elements : elements.slice(0, widget.visibleAvatarCount - 1)"/>
<t t-foreach="visibleElements" t-as="el">
<span><img t-attf-src="/web/image/#{avatarModel}/#{el.id}/#{avatarField}" t-att-title="el.display_name" t-att-data-id="el.id" class="rounded-circle o_m2m_avatar"/></span>
</t>
<t t-if="elements.length > widget.visibleAvatarCount">
<span class="o_m2m_avatar_empty rounded-circle text-center font-weight-bold">
<span t-if="elements.length - widget.visibleAvatarCount + 1 > 9">9+</span>
<span t-else="">+<t t-esc="elements.length - widget.visibleAvatarCount + 1"/></span>
</span>
</t>
</t>
</t>
</t>
<t t-name="Many2ManyTagAvatarPopover">
<t t-foreach="elements" t-as="el">
<div><t t-esc="el.display_name"/></div>
</t>
</t>
<t t-name="FieldMany2ManyTag.colorpicker">
<div class="o_colorpicker dropdown-menu tagcolor_dropdown_menu" role="menu">
<ul>
@@ -4,6 +4,7 @@ odoo.define('web.relational_fields_tests', function (require) {
var AbstractStorageService = require('web.AbstractStorageService');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
const KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var RamStorage = require('web.RamStorage');
var relationalFields = require('web.relational_fields');
@@ -2018,6 +2019,189 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('widget many2many_tags_avatar in list view', async function (assert) {
assert.expect(18);
const records = [];
for (let id = 5; id <= 15; id++) {
records.push({
id,
display_name: `record ${id}`,
});
}
this.data.partner.records = this.data.partner.records.concat(records);
this.data.turtle.records.push({
id: 4,
display_name: "crime master gogo",
turtle_bar: true,
turtle_foo: "yop",
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
});
this.data.turtle.records[0].partner_ids = [1];
this.data.turtle.records[1].partner_ids = [1, 2, 4, 5, 6, 7];
this.data.turtle.records[2].partner_ids = [1, 2, 4, 5, 7];
const list = await createView({
View: ListView,
model: 'turtle',
data: this.data,
arch: '<tree editable="bottom"><field name="partner_ids" widget="many2many_tags_avatar"/></tree>',
});
assert.strictEqual(list.$('.o_data_row:first .o_field_many2manytags img.o_m2m_avatar').data('src'),
"/web/image/partner/1/avatar_128",
"should have correct avatar image");
assert.strictEqual(list.$('.o_data_row:first .o_many2many_tags_avatar_cell .o_field_many2manytags div').text().trim(),
"first record",
"should display like many2one avatar if there is only one record");
assert.containsN(list, '.o_data_row:eq(1) .o_field_many2manytags > span:not(.o_m2m_avatar_empty)', 4,
"should have 4 records");
assert.containsN(list, '.o_data_row:eq(2) .o_field_many2manytags > span:not(.o_m2m_avatar_empty)', 5,
"should have 5 records");
assert.containsOnce(list, '.o_data_row:eq(1) .o_field_many2manytags .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_many2manytags .o_m2m_avatar_empty').text().trim(), "+2",
"should have +2 in o_m2m_avatar_empty");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_many2manytags img.o_m2m_avatar:first').data('src'),
"/web/image/partner/1/avatar_128",
"should have correct avatar image");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_many2manytags img.o_m2m_avatar:eq(1)').data('src'),
"/web/image/partner/2/avatar_128",
"should have correct avatar image");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_many2manytags img.o_m2m_avatar:eq(2)').data('src'),
"/web/image/partner/4/avatar_128",
"should have correct avatar image");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_many2manytags img.o_m2m_avatar:eq(3)').data('src'),
"/web/image/partner/5/avatar_128",
"should have correct avatar image");
assert.containsNone(list, '.o_data_row:eq(2) .o_field_many2manytags .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.containsN(list, '.o_data_row:eq(3) .o_field_many2manytags > span:not(.o_m2m_avatar_empty)', 4,
"should have 4 records");
assert.containsOnce(list, '.o_data_row:eq(3) .o_field_many2manytags .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.strictEqual(list.$('.o_data_row:eq(3) .o_field_many2manytags .o_m2m_avatar_empty').text().trim(), "+9",
"should have +9 in o_m2m_avatar_empty");
list.$('.o_data_row:eq(1) .o_field_many2manytags .o_m2m_avatar_empty').trigger($.Event('mouseenter'));
await testUtils.nextTick();
assert.containsOnce(list, '.popover',
"should open a popover hover on o_m2m_avatar_empty");
assert.strictEqual(list.$('.popover .popover-body > div').text().trim(), "record 6record 7",
"should have a right text in popover");
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_many2many_tags_avatar_cell'));
assert.containsN(list, '.o_data_row.o_selected_row .o_many2many_tags_avatar_cell .badge', 1,
"should have 1 many2many badges in edit mode");
await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
await testUtils.fields.many2one.clickItem('partner_ids', 'second record');
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
assert.containsN(list, '.o_data_row:eq(0) .o_field_many2manytags span', 2,
"should have 2 records");
list.destroy();
});
QUnit.test('widget many2many_tags_avatar in kanban view', async function (assert) {
assert.expect(13);
const records = [];
for (let id = 5; id <= 15; id++) {
records.push({
id,
display_name: `record ${id}`,
});
}
this.data.partner.records = this.data.partner.records.concat(records);
this.data.turtle.records.push({
id: 4,
display_name: "crime master gogo",
turtle_bar: true,
turtle_foo: "yop",
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
});
this.data.turtle.records[0].partner_ids = [1];
this.data.turtle.records[1].partner_ids = [1, 2, 4];
this.data.turtle.records[2].partner_ids = [1, 2, 4, 5];
const kanban = await createView({
View: KanbanView,
model: 'turtle',
data: this.data,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="display_name"/>
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="partner_ids" widget="many2many_tags_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
archs: {
'turtle,false,form': '<form><field name="display_name"/></form>',
},
intercepts: {
switch_view: function (event) {
const { mode, model, res_id, view_type } = event.data;
assert.deepEqual({ mode, model, res_id, view_type }, {
mode: 'readonly',
model: 'turtle',
res_id: 1,
view_type: 'form',
}, "should trigger an event to open the clicked record in a form view");
},
},
});
assert.strictEqual(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar').data('src'),
"/web/image/partner/1/avatar_128",
"should have correct avatar image");
assert.containsN(kanban, '.o_kanban_record:eq(1) .o_field_many2manytags span', 3,
"should have 3 records");
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/partner/1/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/partner/2/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");
assert.containsN(kanban, '.o_kanban_record:eq(3) .o_field_many2manytags > span:not(.o_m2m_avatar_empty)', 2,
"should have 2 records");
assert.containsOnce(kanban, '.o_kanban_record:eq(3) .o_field_many2manytags .o_m2m_avatar_empty',
"should have o_m2m_avatar_empty span");
assert.strictEqual(kanban.$('.o_kanban_record:eq(3) .o_field_many2manytags .o_m2m_avatar_empty').text().trim(), "9+",
"should have 9+ in o_m2m_avatar_empty");
kanban.$('.o_kanban_record:eq(2) .o_field_many2manytags .o_m2m_avatar_empty').trigger($.Event('mouseenter'));
await testUtils.nextTick();
assert.containsOnce(kanban, '.popover',
"should open a popover hover on o_m2m_avatar_empty");
assert.strictEqual(kanban.$('.popover .popover-body > div').text().trim(), "aaarecord 5",
"should have a right text in popover");
await testUtils.dom.click(kanban.$('.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar'));
kanban.destroy();
});
QUnit.test('fieldmany2many tags: quick create a new record', async function (assert) {
assert.expect(3);