[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:
@@ -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");
|
||||
|
||||
+56
-59
@@ -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);
|
||||
|
||||
+14
-1
@@ -6,7 +6,20 @@
|
||||
</img>
|
||||
</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">
|
||||
<attribute name="displayBadge">!props.readonly</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) {
|
||||
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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
+143
-21
@@ -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);
|
||||
|
||||
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-1
@@ -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}"
|
||||
>
|
||||
<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">
|
||||
<Many2XAutocomplete
|
||||
id="props.id"
|
||||
@@ -25,4 +25,42 @@
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user