From 66321f76aa5b7f8b7d328a54907072303cf93088 Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Mon, 27 Feb 2023 08:55:54 +0000 Subject: [PATCH] [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 --- .../static/tests/m2x_avatar_employee_tests.js | 4 +- .../many2many_avatar_user_field.js | 115 ++++++------ .../many2many_avatar_user_field.xml | 15 +- .../m2x_avatar_user_tests.js | 34 ++-- .../views/fields/many2many_tags/tags_list.js | 8 +- .../many2many_tags_avatar_field.js | 164 +++++++++++++++--- .../many2many_tags_avatar_field.scss | 18 ++ .../many2many_tags_avatar_field.xml | 40 ++++- .../many2many_tags_avatar_field_tests.js | 61 +++++-- 9 files changed, 340 insertions(+), 119 deletions(-) diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index 001936f1eb6..acb1e2b9a2f 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -366,8 +366,8 @@ QUnit.module('hr', {}, function () { res_model: 'm2x.avatar.employee', views: [[false, 'kanban']], }); - assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 2, - "should have 2 avatar images"); + assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 3, + "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'), `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`, "should have correct avatar image"); 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 index a2940336605..69fa66c7f8d 100644 --- 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 @@ -1,12 +1,16 @@ /** @odoo-module **/ +import { patch } from "@web/core/utils/patch"; import { registry } from "@web/core/registry"; import { TagsList } from "@web/views/fields/many2many_tags/tags_list"; import { Many2ManyTagsAvatarField, - ListKanbanMany2ManyTagsAvatarField, many2ManyTagsAvatarField, - listKanbanMany2ManyTagsAvatarField, + ListMany2ManyTagsAvatarField, + listMany2ManyTagsAvatarField, + KanbanMany2ManyTagsAvatarField, + kanbanMany2ManyTagsAvatarField, + KanbanMany2ManyTagsAvatarFieldTagsList, } 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"; @@ -14,28 +18,29 @@ import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hoo export class Many2ManyAvatarUserTagsList extends TagsList {} Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList"; -export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField { +const userChatter = { setup() { - super.setup(); + this._super(...arguments); this.openChat = useOpenChat(this.relation); if (this.props.withCommand) { useAssignUserCommand(); } - } + }, - get tags() { - return super.tags.map((tag) => ({ - ...tag, - onImageClicked: () => { - this.openChat(tag.resId); - }, - })); - } -} -Many2ManyTagsAvatarUserField.components = { - ...Many2ManyTagsAvatarField.components, - TagsList: Many2ManyAvatarUserTagsList, + getTagProps(record) { + return { + ...this._super(...arguments), + onImageClicked: () => this.openChat(record.resId), + }; + }, }; +export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField { + static components = { + ...Many2ManyTagsAvatarField.components, + TagsList: Many2ManyAvatarUserTagsList, + }; +} +patch(Many2ManyTagsAvatarUserField.prototype, "mail", userChatter); export const many2ManyTagsAvatarUserField = { ...many2ManyTagsAvatarField, @@ -45,56 +50,48 @@ export const many2ManyTagsAvatarUserField = { registry.category("fields").add("many2many_avatar_user", many2ManyTagsAvatarUserField); -export class KanbanMany2ManyTagsAvatarUserField extends ListKanbanMany2ManyTagsAvatarField { - static props = { - ...ListKanbanMany2ManyTagsAvatarField.props, - displayText: { type: Boolean, optional: true }, +export class KanbanMany2ManyAvatarUserTagsList extends KanbanMany2ManyTagsAvatarFieldTagsList { + static template = "mail.KanbanMany2ManyAvatarUserTagsList"; +} + +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() { - return ( - (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); - }, - })); + return !this.props.readonly; } } -KanbanMany2ManyTagsAvatarUserField.template = "mail.KanbanMany2ManyTagsAvatarUserField"; -KanbanMany2ManyTagsAvatarUserField.components = { - ...ListKanbanMany2ManyTagsAvatarField.components, - TagsList: Many2ManyAvatarUserTagsList, -}; - +patch(KanbanMany2ManyTagsAvatarUserField.prototype, "mail", userChatter); export const kanbanMany2ManyTagsAvatarUserField = { - ...listKanbanMany2ManyTagsAvatarField, + ...kanbanMany2ManyTagsAvatarField, component: KanbanMany2ManyTagsAvatarUserField, - additionalClasses: ["o_field_many2many_tags_avatar"], - extractProps: (fieldInfo) => ({ - ...listKanbanMany2ManyTagsAvatarField.extractProps(fieldInfo), - displayText: fieldInfo.viewType === "list", - }), + additionalClasses: ["o_field_many2many_tags_avatar", "o_field_many2many_tags_avatar_kanban"], +}; +registry.category("fields").add("kanban.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField); + +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", kanbanMany2ManyTagsAvatarUserField); +registry.category("fields").add("list.many2many_avatar_user", listMany2ManyTagsAvatarUserField); registry .category("fields") .add("activity.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField); 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 index abfc51ac305..58193422aba 100644 --- 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 @@ -6,7 +6,20 @@ - + + + tag.onImageClicked + + + + + + !props.readonly + displayText + + + + !props.readonly displayText diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js index 7b70a22b6e7..b3a019e6277 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js @@ -78,11 +78,11 @@ QUnit.module("mail", {}, function () { }); QUnit.test("many2many_avatar_user in kanban view", async function (assert) { - assert.expect(5); + assert.expect(8); patchWithCleanup(browser, { setTimeout: async (fn) => { - await new Promise((r) => setTimeout(r)) + await new Promise((r) => setTimeout(r)); fn(); }, }); @@ -136,26 +136,34 @@ QUnit.module("mail", {}, function () { "+2", "should have +2 in o_m2m_avatar_empty" ); - - document - .querySelector(".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty") - .dispatchEvent(new Event("mouseenter")); - await nextTick(); + await click( + document.querySelector( + ".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty" + ) + ); assert.containsOnce( document.body, ".popover", "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( - 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", "should have a right text in popover" ); - assert.strictEqual( - document.querySelectorAll(".popover .o-tooltip > div")[1].innerText.trim(), - "Tapu", - "should have a right text in popover" - ); + assert.strictEqual(tags[3].innerText.trim(), "Tapu", "should have a right text in popover"); }); QUnit.test( diff --git a/addons/web/static/src/views/fields/many2many_tags/tags_list.js b/addons/web/static/src/views/fields/many2many_tags/tags_list.js index 0ba482aa122..77690758d22 100644 --- a/addons/web/static/src/views/fields/many2many_tags/tags_list.js +++ b/addons/web/static/src/views/fields/many2many_tags/tags_list.js @@ -17,10 +17,12 @@ export class TagsList extends Component { itemsVisible: { type: Number, optional: true }, tags: { type: Object, optional: true }, }; - + get visibleTagsCount() { + return this.props.itemsVisible - 1; + } get visibleTags() { 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; } @@ -28,7 +30,7 @@ export class TagsList extends Component { if (!this.props.itemsVisible || this.props.tags.length <= this.props.itemsVisible) { return []; } - return this.props.tags.slice(this.props.itemsVisible - 1); + return this.props.tags.slice(this.visibleTagsCount); } get tooltipInfo() { return JSON.stringify({ diff --git a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js index 54697cb3ef4..baf3ef89b96 100644 --- a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js +++ b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js @@ -1,5 +1,7 @@ /** @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 { Many2XAutocomplete } from "@web/views/fields/relational_utils"; import { @@ -7,6 +9,7 @@ import { Many2ManyTagsField, } from "@web/views/fields/many2many_tags/many2many_tags_field"; import { TagsList } from "../many2many_tags/tags_list"; +import { onMounted, useRef, useState } from "@odoo/owl"; export class Many2ManyTagsAvatarField extends Many2ManyTagsField { static template = "web.Many2ManyTagsAvatarField"; @@ -18,13 +21,11 @@ export class Many2ManyTagsAvatarField extends Many2ManyTagsField { ...Many2ManyTagsField.props, withCommand: { type: Boolean, optional: true }, }; - - get tags() { - return super.tags.map((tag) => ({ - ...tag, - img: `/web/image/${this.relation}/${tag.resId}/avatar_128`, - onDelete: !this.props.readonly ? () => this.deleteTag(tag.id) : undefined, - })); + getTagProps(record) { + return { + ...super.getTagProps(record), + img: `/web/image/${this.relation}/${record.resId}/avatar_128`, + }; } } @@ -39,32 +40,153 @@ export const 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 = { ...Many2ManyTagsAvatarField.props, - itemsVisible: { type: Number, optional: true }, + close: { type: Function }, + deleteTag: { type: Function }, + updateTag: { type: Function }, }; - static defaultProps = { - ...Many2ManyTagsAvatarField.defaultProps, - itemsVisible: 3, + + setup() { + 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) { return { ...super.getTagProps(record), - img: `/web/image/${this.relation}/${record.resId}/avatar_128`, + onDelete: () => this.deleteTag(record.id), }; } } -export const listKanbanMany2ManyTagsAvatarField = { +export const kanbanMany2ManyTagsAvatarField = { ...many2ManyTagsAvatarField, - component: ListKanbanMany2ManyTagsAvatarField, - extractProps: (fieldInfo) => ({ - ...many2ManyTagsAvatarField.extractProps(fieldInfo), - itemsVisible: fieldInfo.viewType === "list" ? 5 : 3, - }), + component: KanbanMany2ManyTagsAvatarField, }; -registry.category("fields").add("list.many2many_tags_avatar", listKanbanMany2ManyTagsAvatarField); -registry.category("fields").add("kanban.many2many_tags_avatar", listKanbanMany2ManyTagsAvatarField); +registry.category("fields").add("kanban.many2many_tags_avatar", kanbanMany2ManyTagsAvatarField); diff --git a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.scss b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.scss index 490a1cd8307..443dae38a7f 100644 --- a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.scss +++ b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.scss @@ -80,3 +80,21 @@ 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; + } +} diff --git a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.xml b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.xml index 03aa3388858..9ee33498cb3 100644 --- a/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.xml +++ b/addons/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.xml @@ -6,7 +6,7 @@ class="o_field_tags d-inline-flex flex-wrap mw-100" t-att-class="{'o_tags_input o_input': !props.readonly}" > - +
+ + + Quick assign + + + emptyAvatar + openPopover + + + {'o_allow_delete_hover': canDisplayDelete} + + + + Remove + Remove + + + + + + popoverProps + isFieldReadonly + + + + + + + + + + + + + + diff --git a/addons/web/static/tests/views/fields/many2many_tags_avatar_field_tests.js b/addons/web/static/tests/views/fields/many2many_tags_avatar_field_tests.js index baac904eebb..e76e05bc731 100644 --- a/addons/web/static/tests/views/fields/many2many_tags_avatar_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_tags_avatar_field_tests.js @@ -1,6 +1,12 @@ /** @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 { triggerHotkey } from "../../helpers/utils"; @@ -273,7 +279,7 @@ QUnit.module("Fields", (hooks) => { ); QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) { - assert.expect(13); + assert.expect(16); const records = []; for (let id = 5; id <= 15; id++) { @@ -336,8 +342,8 @@ QUnit.module("Fields", (hooks) => { assert.containsN( target, ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag", - 3, - "should have 3 records" + 2, + "should have 2 records" ); assert.containsN( target, @@ -395,25 +401,42 @@ QUnit.module("Fields", (hooks) => { "should have 9+ in o_m2m_avatar_empty" ); - // check data-tooltip attribute (used by the tooltip service) - const tag = target.querySelector( - ".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty" + // check delete + const firstTag = target.querySelector( + ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag" ); - assert.strictEqual( - tag.dataset["tooltipTemplate"], - "web.TagsList.Tooltip", - "uses the proper tooltip template" - ); - const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]); - assert.strictEqual( - tooltipInfo.tags.map((tag) => tag.text).join(" "), - "aaa record 5", - "shows a tooltip on hover" + await triggerEvent(firstTag, null, "mouseover"); + await click(firstTag, ".o_delete"); + assert.containsN( + target, + ".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag", + 2, + "should have 2 record" ); - await click( - target.querySelector(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar") + const o_kanban_record = target.querySelector(".o_kanban_record:nth-child(2)"); + 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) {