[IMP] web,mail: many2many avatar quick assign

Purpose:

Allow quick assignation of records in the kanban views (and other places
by extension).
Currently, if you want to work in a board-like env (e.g. tasks in
kanban, crm in pipeline, that kind of thing), having the possibility to
quickly assign/unassign records without having to open them fullscreen
can be really useful => it should be possible to "quick edit" these
fields in kanban views easily, without switching to form views.

If you want to keep the field readonly don't forget to add the readonly
attribute.

Task-id: 3121207
Part-of: odoo/odoo#114400
This commit is contained in:
Romain Estievenart
2023-03-10 17:54:56 +01:00
parent 7bd93cc64b
commit 66321f76aa
9 changed files with 340 additions and 119 deletions
@@ -366,8 +366,8 @@ QUnit.module('hr', {}, function () {
res_model: 'm2x.avatar.employee', res_model: 'm2x.avatar.employee',
views: [[false, 'kanban']], views: [[false, 'kanban']],
}); });
assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 2, assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 3,
"should have 2 avatar images"); "should have 2 avatar images and the default empty avatar");
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_employee 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`, `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
"should have correct avatar image"); "should have correct avatar image");
@@ -1,12 +1,16 @@
/** @odoo-module **/ /** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { registry } from "@web/core/registry"; import { registry } from "@web/core/registry";
import { TagsList } from "@web/views/fields/many2many_tags/tags_list"; import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
import { import {
Many2ManyTagsAvatarField, Many2ManyTagsAvatarField,
ListKanbanMany2ManyTagsAvatarField,
many2ManyTagsAvatarField, many2ManyTagsAvatarField,
listKanbanMany2ManyTagsAvatarField, ListMany2ManyTagsAvatarField,
listMany2ManyTagsAvatarField,
KanbanMany2ManyTagsAvatarField,
kanbanMany2ManyTagsAvatarField,
KanbanMany2ManyTagsAvatarFieldTagsList,
} from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field"; } from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field";
import { useOpenChat } from "@mail/views/open_chat_hook"; import { useOpenChat } from "@mail/views/open_chat_hook";
import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook"; import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook";
@@ -14,28 +18,29 @@ import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hoo
export class Many2ManyAvatarUserTagsList extends TagsList {} export class Many2ManyAvatarUserTagsList extends TagsList {}
Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList"; Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList";
export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField { const userChatter = {
setup() { setup() {
super.setup(); this._super(...arguments);
this.openChat = useOpenChat(this.relation); this.openChat = useOpenChat(this.relation);
if (this.props.withCommand) { if (this.props.withCommand) {
useAssignUserCommand(); useAssignUserCommand();
} }
} },
get tags() { getTagProps(record) {
return super.tags.map((tag) => ({ return {
...tag, ...this._super(...arguments),
onImageClicked: () => { onImageClicked: () => this.openChat(record.resId),
this.openChat(tag.resId); };
}, },
}));
}
}
Many2ManyTagsAvatarUserField.components = {
...Many2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
}; };
export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField {
static components = {
...Many2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
};
}
patch(Many2ManyTagsAvatarUserField.prototype, "mail", userChatter);
export const many2ManyTagsAvatarUserField = { export const many2ManyTagsAvatarUserField = {
...many2ManyTagsAvatarField, ...many2ManyTagsAvatarField,
@@ -45,56 +50,48 @@ export const many2ManyTagsAvatarUserField = {
registry.category("fields").add("many2many_avatar_user", many2ManyTagsAvatarUserField); registry.category("fields").add("many2many_avatar_user", many2ManyTagsAvatarUserField);
export class KanbanMany2ManyTagsAvatarUserField extends ListKanbanMany2ManyTagsAvatarField { export class KanbanMany2ManyAvatarUserTagsList extends KanbanMany2ManyTagsAvatarFieldTagsList {
static props = { static template = "mail.KanbanMany2ManyAvatarUserTagsList";
...ListKanbanMany2ManyTagsAvatarField.props, }
displayText: { type: Boolean, optional: true },
export class KanbanMany2ManyTagsAvatarUserField extends KanbanMany2ManyTagsAvatarField {
static template = "mail.KanbanMany2ManyTagsAvatarUserField";
static components = {
...KanbanMany2ManyTagsAvatarField.components,
TagsList: KanbanMany2ManyAvatarUserTagsList,
}; };
setup() {
super.setup();
this.openChat = useOpenChat(this.relation);
if (this.props.withCommand) {
useAssignUserCommand();
}
}
get displayText() { get displayText() {
return ( return !this.props.readonly;
(this.props.displayText && this.props.record.data[this.props.name].records.length === 1) ||
!this.props.readonly
);
}
get tags() {
const recordFromId = (id) =>
this.props.record.data[this.props.name].records.find((rec) => rec.id === id);
return super.tags.map((tag) => ({
...tag,
onImageClicked: () => {
this.openChat(recordFromId(tag.id).resId);
},
}));
} }
} }
KanbanMany2ManyTagsAvatarUserField.template = "mail.KanbanMany2ManyTagsAvatarUserField"; patch(KanbanMany2ManyTagsAvatarUserField.prototype, "mail", userChatter);
KanbanMany2ManyTagsAvatarUserField.components = {
...ListKanbanMany2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
};
export const kanbanMany2ManyTagsAvatarUserField = { export const kanbanMany2ManyTagsAvatarUserField = {
...listKanbanMany2ManyTagsAvatarField, ...kanbanMany2ManyTagsAvatarField,
component: KanbanMany2ManyTagsAvatarUserField, component: KanbanMany2ManyTagsAvatarUserField,
additionalClasses: ["o_field_many2many_tags_avatar"], additionalClasses: ["o_field_many2many_tags_avatar", "o_field_many2many_tags_avatar_kanban"],
extractProps: (fieldInfo) => ({ };
...listKanbanMany2ManyTagsAvatarField.extractProps(fieldInfo), registry.category("fields").add("kanban.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField);
displayText: fieldInfo.viewType === "list",
}), export class ListMany2ManyTagsAvatarUserField extends ListMany2ManyTagsAvatarField {
static template = "mail.ListMany2ManyTagsAvatarUserField";
static components = {
...ListMany2ManyTagsAvatarField.components,
TagsList: Many2ManyAvatarUserTagsList,
};
get displayText() {
return this.props.record.data[this.props.name].records.length === 1 || !this.props.readonly;
}
}
patch(ListMany2ManyTagsAvatarUserField.prototype, "mail", userChatter);
export const listMany2ManyTagsAvatarUserField = {
...listMany2ManyTagsAvatarField,
component: ListMany2ManyTagsAvatarUserField,
additionalClasses: ["o_field_many2many_tags_avatar", "o_field_many2many_tags_avatar_list"],
}; };
registry.category("fields").add("kanban.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField); registry.category("fields").add("list.many2many_avatar_user", listMany2ManyTagsAvatarUserField);
registry.category("fields").add("list.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField);
registry registry
.category("fields") .category("fields")
.add("activity.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField); .add("activity.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField);
@@ -6,7 +6,20 @@
</img> </img>
</t> </t>
<t t-name="mail.KanbanMany2ManyTagsAvatarUserField" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1"> <t t-name="mail.KanbanMany2ManyAvatarUserTagsList" t-inherit="web.KanbanMany2ManyTagsAvatarFieldTagsList" t-inherit-mode="primary" owl="1">
<img position="attributes">
<attribute name="t-on-click.stop.prevent">tag.onImageClicked</attribute>
</img>
</t>
<t t-name="mail.ListMany2ManyTagsAvatarUserField" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
<TagsList position="attributes">
<attribute name="displayBadge">!props.readonly</attribute>
<attribute name="displayText">displayText</attribute>
</TagsList>
</t>
<t t-name="mail.KanbanMany2ManyTagsAvatarUserField" t-inherit="web.KanbanMany2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
<TagsList position="attributes"> <TagsList position="attributes">
<attribute name="displayBadge">!props.readonly</attribute> <attribute name="displayBadge">!props.readonly</attribute>
<attribute name="displayText">displayText</attribute> <attribute name="displayText">displayText</attribute>
@@ -78,11 +78,11 @@ QUnit.module("mail", {}, function () {
}); });
QUnit.test("many2many_avatar_user in kanban view", async function (assert) { QUnit.test("many2many_avatar_user in kanban view", async function (assert) {
assert.expect(5); assert.expect(8);
patchWithCleanup(browser, { patchWithCleanup(browser, {
setTimeout: async (fn) => { setTimeout: async (fn) => {
await new Promise((r) => setTimeout(r)) await new Promise((r) => setTimeout(r));
fn(); fn();
}, },
}); });
@@ -136,26 +136,34 @@ QUnit.module("mail", {}, function () {
"+2", "+2",
"should have +2 in o_m2m_avatar_empty" "should have +2 in o_m2m_avatar_empty"
); );
await click(
document document.querySelector(
.querySelector(".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty") ".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty"
.dispatchEvent(new Event("mouseenter")); )
await nextTick(); );
assert.containsOnce( assert.containsOnce(
document.body, document.body,
".popover", ".popover",
"should open a popover hover on o_m2m_avatar_empty" "should open a popover hover on o_m2m_avatar_empty"
); );
const tags = document.querySelectorAll(".o_popover > .o_field_tags > .o_tag");
assert.strictEqual(tags.length, 4, "should have 4 tags");
assert.strictEqual( assert.strictEqual(
document.querySelector(".popover .o-tooltip > div").innerText.trim(), tags[0].innerText.trim(),
"Mario",
"should have a right text in popover"
);
assert.strictEqual(
tags[1].innerText.trim(),
"Yoshi",
"should have a right text in popover"
);
assert.strictEqual(
tags[2].innerText.trim(),
"Luigi", "Luigi",
"should have a right text in popover" "should have a right text in popover"
); );
assert.strictEqual( assert.strictEqual(tags[3].innerText.trim(), "Tapu", "should have a right text in popover");
document.querySelectorAll(".popover .o-tooltip > div")[1].innerText.trim(),
"Tapu",
"should have a right text in popover"
);
}); });
QUnit.test( QUnit.test(
@@ -17,10 +17,12 @@ export class TagsList extends Component {
itemsVisible: { type: Number, optional: true }, itemsVisible: { type: Number, optional: true },
tags: { type: Object, optional: true }, tags: { type: Object, optional: true },
}; };
get visibleTagsCount() {
return this.props.itemsVisible - 1;
}
get visibleTags() { get visibleTags() {
if (this.props.itemsVisible && this.props.tags.length > this.props.itemsVisible) { if (this.props.itemsVisible && this.props.tags.length > this.props.itemsVisible) {
return this.props.tags.slice(0, this.props.itemsVisible - 1); return this.props.tags.slice(0, this.visibleTagsCount);
} }
return this.props.tags; return this.props.tags;
} }
@@ -28,7 +30,7 @@ export class TagsList extends Component {
if (!this.props.itemsVisible || this.props.tags.length <= this.props.itemsVisible) { if (!this.props.itemsVisible || this.props.tags.length <= this.props.itemsVisible) {
return []; return [];
} }
return this.props.tags.slice(this.props.itemsVisible - 1); return this.props.tags.slice(this.visibleTagsCount);
} }
get tooltipInfo() { get tooltipInfo() {
return JSON.stringify({ return JSON.stringify({
@@ -1,5 +1,7 @@
/** @odoo-module **/ /** @odoo-module **/
import { isMobileOS } from "@web/core/browser/feature_detection";
import { usePopover } from "@web/core/popover/popover_hook";
import { registry } from "@web/core/registry"; import { registry } from "@web/core/registry";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils"; import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { import {
@@ -7,6 +9,7 @@ import {
Many2ManyTagsField, Many2ManyTagsField,
} from "@web/views/fields/many2many_tags/many2many_tags_field"; } from "@web/views/fields/many2many_tags/many2many_tags_field";
import { TagsList } from "../many2many_tags/tags_list"; import { TagsList } from "../many2many_tags/tags_list";
import { onMounted, useRef, useState } from "@odoo/owl";
export class Many2ManyTagsAvatarField extends Many2ManyTagsField { export class Many2ManyTagsAvatarField extends Many2ManyTagsField {
static template = "web.Many2ManyTagsAvatarField"; static template = "web.Many2ManyTagsAvatarField";
@@ -18,13 +21,11 @@ export class Many2ManyTagsAvatarField extends Many2ManyTagsField {
...Many2ManyTagsField.props, ...Many2ManyTagsField.props,
withCommand: { type: Boolean, optional: true }, withCommand: { type: Boolean, optional: true },
}; };
getTagProps(record) {
get tags() { return {
return super.tags.map((tag) => ({ ...super.getTagProps(record),
...tag, img: `/web/image/${this.relation}/${record.resId}/avatar_128`,
img: `/web/image/${this.relation}/${tag.resId}/avatar_128`, };
onDelete: !this.props.readonly ? () => this.deleteTag(tag.id) : undefined,
}));
} }
} }
@@ -39,32 +40,153 @@ export const many2ManyTagsAvatarField = {
registry.category("fields").add("many2many_tags_avatar", many2ManyTagsAvatarField); registry.category("fields").add("many2many_tags_avatar", many2ManyTagsAvatarField);
export class ListKanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField { export class ListMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
itemsVisible = 5;
}
export const listMany2ManyTagsAvatarField = {
...many2ManyTagsAvatarField,
component: ListMany2ManyTagsAvatarField,
};
registry.category("fields").add("list.many2many_tags_avatar", listMany2ManyTagsAvatarField);
export class Many2ManyTagsAvatarFieldPopover extends Many2ManyTagsAvatarField {
static template = "web.Many2ManyTagsAvatarFieldPopover";
static props = { static props = {
...Many2ManyTagsAvatarField.props, ...Many2ManyTagsAvatarField.props,
itemsVisible: { type: Number, optional: true }, close: { type: Function },
deleteTag: { type: Function },
updateTag: { type: Function },
}; };
static defaultProps = {
...Many2ManyTagsAvatarField.defaultProps, setup() {
itemsVisible: 3, super.setup();
this.state = useState({ tags: this.tags });
this.update = async (recordList) => {
const updatedVal = await this.props.updateTag(recordList);
this.state.tags = updatedVal.map((tag) => ({
...tag,
onDelete: () => this.deleteTag(tag.id),
}));
};
onMounted(() => {
this.autoCompleteRef.el.querySelector("input").focus();
});
}
async deleteTag(id) {
const updatedVal = await this.props.deleteTag(id);
this.state.tags = updatedVal.map((tag) => ({
...tag,
onDelete: () => this.deleteTag(tag.id),
}));
}
}
export const many2ManyTagsAvatarFieldPopover = {
...many2ManyTagsAvatarField,
component: Many2ManyTagsAvatarFieldPopover,
};
registry.category("fields").add("many2many_tags_avatar_popover", many2ManyTagsAvatarFieldPopover);
export class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {
static template = "web.KanbanMany2ManyTagsAvatarFieldTagsList";
static props = {
...TagsList.props,
popoverProps: { type: Object },
readonly: { type: Boolean, optional: true },
}; };
setup() {
super.setup();
this.popover = usePopover();
this.emptyAvatar = useRef("emptyAvatar");
}
get visibleTagsCount() {
return this.props.itemsVisible;
}
closePopover() {
this.closePopoverFn();
this.closePopoverFn = null;
}
openPopover(ev) {
if (this.props.readonly) {
return;
}
if (this.closePopoverFn) {
this.closePopover();
}
this.emptyAvatar.el?.dispatchEvent(new MouseEvent("mouseleave"));
this.emptyAvatar.el?.removeAttribute("data-tooltip-template");
this.closePopoverFn = this.popover.add(
ev.currentTarget.parentElement,
Many2ManyTagsAvatarFieldPopover,
{
...this.props.popoverProps,
readonly: false,
canCreate: false,
canCreateEdit: false,
canQuickCreate: false,
},
{
position: "bottom",
onClose: () => {
this.emptyAvatar.el?.setAttribute(
"data-tooltip-template",
"web.TagsList.Tooltip"
);
},
}
);
}
get canDisplayDelete() {
return !this.props.readonly && !isMobileOS();
}
}
export class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
static template = "web.KanbanMany2ManyTagsAvatarField";
static components = {
...Many2ManyTagsAvatarField.component,
TagsList: KanbanMany2ManyTagsAvatarFieldTagsList,
};
itemsVisible = 2;
get isFieldReadonly() {
return this.props.record.isReadonly(this.props.name);
}
get popoverProps() {
return {
...this.props,
readonly: this.isFieldReadonly,
deleteTag: this.deleteTag.bind(this),
updateTag: this.updateTag.bind(this),
};
}
async deleteTag(id) {
super.deleteTag(id);
await this.props.record.save({ noReload: true });
return this.tags;
}
async updateTag(recordList) {
await this.update(recordList);
await this.props.record.save({ noReload: true });
return this.tags;
}
getTagProps(record) { getTagProps(record) {
return { return {
...super.getTagProps(record), ...super.getTagProps(record),
img: `/web/image/${this.relation}/${record.resId}/avatar_128`, onDelete: () => this.deleteTag(record.id),
}; };
} }
} }
export const listKanbanMany2ManyTagsAvatarField = { export const kanbanMany2ManyTagsAvatarField = {
...many2ManyTagsAvatarField, ...many2ManyTagsAvatarField,
component: ListKanbanMany2ManyTagsAvatarField, component: KanbanMany2ManyTagsAvatarField,
extractProps: (fieldInfo) => ({
...many2ManyTagsAvatarField.extractProps(fieldInfo),
itemsVisible: fieldInfo.viewType === "list" ? 5 : 3,
}),
}; };
registry.category("fields").add("list.many2many_tags_avatar", listKanbanMany2ManyTagsAvatarField); registry.category("fields").add("kanban.many2many_tags_avatar", kanbanMany2ManyTagsAvatarField);
registry.category("fields").add("kanban.many2many_tags_avatar", listKanbanMany2ManyTagsAvatarField);
@@ -80,3 +80,21 @@
flex-basis: 40px; flex-basis: 40px;
} }
} }
.o_m2m_tags_avatar_field_popover {
max-width: 280px;
.dropdown-menu {
width: 275px;
}
}
.o_field_many2many_tags_avatar_kanban {
.o_field_tags .o_tag .o_delete {
margin-left: -12px;
margin-top: -4px;
width: 12px;
height: 12px;
visibility: hidden;
}
.o_allow_delete_hover:hover .o_delete {
visibility: visible;
}
}
@@ -6,7 +6,7 @@
class="o_field_tags d-inline-flex flex-wrap mw-100" class="o_field_tags d-inline-flex flex-wrap mw-100"
t-att-class="{'o_tags_input o_input': !props.readonly}" t-att-class="{'o_tags_input o_input': !props.readonly}"
> >
<TagsList tags="tags" itemsVisible="props.itemsVisible"/> <TagsList tags="tags" itemsVisible="itemsVisible"/>
<div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-flex w-100" t-on-keydown="onAutoCompleteKeydown" t-ref="autoComplete"> <div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-flex w-100" t-on-keydown="onAutoCompleteKeydown" t-ref="autoComplete">
<Many2XAutocomplete <Many2XAutocomplete
id="props.id" id="props.id"
@@ -25,4 +25,42 @@
</div> </div>
</t> </t>
<t t-name="web.KanbanMany2ManyTagsAvatarFieldTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_m2m_avatar_empty')]" position="after">
<img t-elif="!props.readonly" src="/web/static/img/user_menu_avatar.png" title="Quick assign" alt="Quick assign"
t-on-click.stop.prevent="openPopover" class="o_m2m_avatar rounded-circle align-self-center"/>
</xpath>
<xpath expr="//span[hasclass('o_m2m_avatar_empty')]" position="attributes">
<attribute name="t-ref">emptyAvatar</attribute>
<attribute name="t-on-click.stop.prevent">openPopover</attribute>
</xpath>
<xpath expr="//span[@t-att-title='tag.text']" position="attributes">
<attribute name="t-att-class">{'o_allow_delete_hover': canDisplayDelete}</attribute>
</xpath>
<xpath expr="//a[hasclass('o_delete')]" position="attributes">
<attribute name="class" remove="fa-times" add="fa-times-circle fs-6 text-danger bg-white align-self-start z-index-1 pt-0 rounded-circle" separator=" " />
<attribute name="title">Remove</attribute>
<attribute name="aria-label">Remove</attribute>
</xpath>
</t>
<t t-name="web.KanbanMany2ManyTagsAvatarField" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary"
owl="1">
<xpath expr="//TagsList" position="attributes">
<attribute name="popoverProps">popoverProps</attribute>
<attribute name="readonly">isFieldReadonly</attribute>
</xpath>
</t>
<t t-name="web.Many2ManyTagsAvatarFieldPopover" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_field_tags')]" position="attributes">
<attribute name="class" add="p-1 o_m2m_tags_avatar_field_popover o_field_widget o_field_many2many_tags_avatar" remove="d-inline-flex mw-100" separator=" " />
</xpath>
<xpath expr="//div[hasclass('o_field_many2many_selection')]" position="attributes">
<attribute name="class" add="w-100" separator=" " />
</xpath>
<xpath expr="//TagsList" position="attributes">
<attribute name="tags" remove="tags" add="state.tags" />
</xpath>
</t>
</templates> </templates>
@@ -1,6 +1,12 @@
/** @odoo-module **/ /** @odoo-module **/
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils"; import {
click,
clickSave,
getFixture,
triggerEvent,
selectDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { triggerHotkey } from "../../helpers/utils"; import { triggerHotkey } from "../../helpers/utils";
@@ -273,7 +279,7 @@ QUnit.module("Fields", (hooks) => {
); );
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) { QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
assert.expect(13); assert.expect(16);
const records = []; const records = [];
for (let id = 5; id <= 15; id++) { for (let id = 5; id <= 15; id++) {
@@ -336,8 +342,8 @@ QUnit.module("Fields", (hooks) => {
assert.containsN( assert.containsN(
target, target,
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag", ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
3, 2,
"should have 3 records" "should have 2 records"
); );
assert.containsN( assert.containsN(
target, target,
@@ -395,25 +401,42 @@ QUnit.module("Fields", (hooks) => {
"should have 9+ in o_m2m_avatar_empty" "should have 9+ in o_m2m_avatar_empty"
); );
// check data-tooltip attribute (used by the tooltip service) // check delete
const tag = target.querySelector( const firstTag = target.querySelector(
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty" ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag"
); );
assert.strictEqual( await triggerEvent(firstTag, null, "mouseover");
tag.dataset["tooltipTemplate"], await click(firstTag, ".o_delete");
"web.TagsList.Tooltip", assert.containsN(
"uses the proper tooltip template" target,
); ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]); 2,
assert.strictEqual( "should have 2 record"
tooltipInfo.tags.map((tag) => tag.text).join(" "),
"aaa record 5",
"shows a tooltip on hover"
); );
await click( const o_kanban_record = target.querySelector(".o_kanban_record:nth-child(2)");
target.querySelector(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar") await click(o_kanban_record, ".o_field_tags > img.o_m2m_avatar", true);
const popover = document.querySelector(".o_popover_container");
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tags");
// delete inside the popover
await click(popover.querySelector(".o_tag .o_delete"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 1, "Should have 1 tag");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
1,
"Should have 1 tags"
); );
// select input
await click(popover.querySelector(".o_field_many2many_selection input"));
// select first input
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tags");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
await click(target);
}); });
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) { QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {