[IMP] mail,hr,web: change the UX of the Many2ManyAvatarFieldPopover
The commit change the design of the Many2ManyAvatarFieldPopover the goal is to remove the m2m dropdown and fill the dropdown content directly inside the popover. It also reverses the order of the displayed tags inside the m2m avatar field in the kanban view and inside the popover. It removes the possibility to unassign users on mouse hover in kanban view. Note: It's still possible to remove users inside an m2m_avatar_field popover. Task ID: 3229888 closes odoo/odoo#117194 X-original-commit: f0d71e98743421318a798ef9d4f1e75d11a23334 Signed-off-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
+9
-1
@@ -6,6 +6,7 @@ import {
|
||||
KanbanMany2ManyTagsAvatarUserField,
|
||||
many2ManyTagsAvatarUserField,
|
||||
kanbanMany2ManyTagsAvatarUserField,
|
||||
listMany2ManyTagsAvatarUserField,
|
||||
} from "@mail/web/fields/many2many_avatar_user_field/many2many_avatar_user_field";
|
||||
|
||||
export class Many2ManyTagsAvatarEmployeeField extends Many2ManyTagsAvatarUserField {
|
||||
@@ -43,6 +44,13 @@ export const kanbanMany2ManyTagsAvatarEmployeeField = {
|
||||
registry
|
||||
.category("fields")
|
||||
.add("kanban.many2many_avatar_employee", kanbanMany2ManyTagsAvatarEmployeeField);
|
||||
export const listMany2ManyTagsAvatarEmployeeField = {
|
||||
...listMany2ManyTagsAvatarUserField,
|
||||
additionalClasses: [
|
||||
...listMany2ManyTagsAvatarUserField.additionalClasses,
|
||||
"o_field_many2many_avatar_user",
|
||||
],
|
||||
};
|
||||
registry
|
||||
.category("fields")
|
||||
.add("list.many2many_avatar_employee", kanbanMany2ManyTagsAvatarEmployeeField);
|
||||
.add("list.many2many_avatar_employee", listMany2ManyTagsAvatarEmployeeField);
|
||||
|
||||
@@ -347,14 +347,14 @@ QUnit.test("many2many_avatar_employee widget in kanban view", async function (as
|
||||
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"
|
||||
2,
|
||||
"should have 2 avatar images"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document
|
||||
.querySelector(".o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar")
|
||||
.getAttribute("data-src"),
|
||||
`/web/image/hr.employee.public/${employeeId_1}/avatar_128`
|
||||
`/web/image/hr.employee.public/${employeeId_2}/avatar_128`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
document
|
||||
@@ -362,11 +362,11 @@ QUnit.test("many2many_avatar_employee widget in kanban view", async function (as
|
||||
".o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar"
|
||||
)[1]
|
||||
.getAttribute("data-src"),
|
||||
`/web/image/hr.employee.public/${employeeId_2}/avatar_128`
|
||||
`/web/image/hr.employee.public/${employeeId_1}/avatar_128`,
|
||||
);
|
||||
|
||||
await dom.click(document.querySelector(".o_kanban_record .o_m2m_avatar"));
|
||||
await dom.click(document.querySelectorAll(".o_kanban_record .o_m2m_avatar")[1]);
|
||||
await dom.click(document.querySelectorAll('.o_kanban_record img.o_m2m_avatar')[1]);
|
||||
await dom.click(document.querySelectorAll('.o_kanban_record img.o_m2m_avatar')[0]);
|
||||
assert.verifySteps([
|
||||
`read hr.employee.public ${employeeId_1},${employeeId_2}`,
|
||||
`read hr.employee.public ${employeeId_1}`,
|
||||
|
||||
@@ -59,10 +59,10 @@ QUnit.test("many2many_avatar_user in kanban view", async (assert) => {
|
||||
);
|
||||
const tags = document.querySelectorAll(".o_popover > .o_field_tags > .o_tag");
|
||||
assert.strictEqual(tags.length, 4);
|
||||
assert.strictEqual(tags[0].innerText.trim(), "Mario");
|
||||
assert.strictEqual(tags[1].innerText.trim(), "Yoshi");
|
||||
assert.strictEqual(tags[2].innerText.trim(), "Luigi");
|
||||
assert.strictEqual(tags[3].innerText.trim(), "Tapu");
|
||||
assert.strictEqual(tags[0].innerText.trim(), "Tapu");
|
||||
assert.strictEqual(tags[1].innerText.trim(), "Luigi");
|
||||
assert.strictEqual(tags[2].innerText.trim(), "Yoshi");
|
||||
assert.strictEqual(tags[3].innerText.trim(), "Mario");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
|
||||
@@ -41,10 +41,14 @@ export class AutoComplete extends Component {
|
||||
});
|
||||
|
||||
// position and size
|
||||
usePosition(() => this.inputRef.el, {
|
||||
popper: "sourcesList",
|
||||
position: "bottom-start",
|
||||
});
|
||||
if (this.props.dropdown) {
|
||||
usePosition(() => this.inputRef.el, {
|
||||
popper: "sourcesList",
|
||||
position: "bottom-start",
|
||||
});
|
||||
} else {
|
||||
this.open(false);
|
||||
}
|
||||
}
|
||||
|
||||
get isOpened() {
|
||||
@@ -99,6 +103,9 @@ export class AutoComplete extends Component {
|
||||
this.navigate(0);
|
||||
});
|
||||
}
|
||||
get displayOptions() {
|
||||
return !this.props.dropdown || (this.isOpened && this.hasOptions);
|
||||
}
|
||||
loadOptions(options, request) {
|
||||
if (typeof options === "function") {
|
||||
return options(request);
|
||||
@@ -234,7 +241,16 @@ export class AutoComplete extends Component {
|
||||
this.inputRef.el.setSelectionRange(0, this.inputRef.el.value.length);
|
||||
this.props.onFocus(ev);
|
||||
}
|
||||
|
||||
get autoCompleteRootClass() {
|
||||
let classList = "";
|
||||
if (this.props.class) {
|
||||
classList += this.props.class;
|
||||
}
|
||||
if (this.props.dropdown) {
|
||||
classList += " dropdown";
|
||||
}
|
||||
return classList;
|
||||
}
|
||||
onInputKeydown(ev) {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
switch (hotkey) {
|
||||
@@ -324,12 +340,14 @@ Object.assign(AutoComplete, {
|
||||
onBlur: { type: Function, optional: true },
|
||||
onFocus: { type: Function, optional: true },
|
||||
input: { type: Function, optional: true },
|
||||
dropdown: { type: Boolean, optional: true },
|
||||
autofocus: { type: Boolean, optional: true },
|
||||
class: { type: String, optional: true },
|
||||
},
|
||||
defaultProps: {
|
||||
placeholder: "",
|
||||
autoSelect: false,
|
||||
dropdown: true,
|
||||
onInput: () => {},
|
||||
onChange: () => {},
|
||||
onBlur: () => {},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.AutoComplete" owl="1">
|
||||
<div class="o-autocomplete dropdown" t-ref="root" t-att-class="props.class">
|
||||
<div class="o-autocomplete" t-ref="root" t-att-class="autoCompleteRootClass">
|
||||
<input
|
||||
type="text"
|
||||
t-att-id="props.id"
|
||||
@@ -18,11 +18,20 @@
|
||||
t-on-focus="onInputFocus"
|
||||
t-ref="input"
|
||||
/>
|
||||
<t t-if="isOpened and hasOptions">
|
||||
<ul class="o-autocomplete--dropdown-menu dropdown-menu ui-widget ui-autocomplete show" t-on-mousedown.prevent="" t-ref="sourcesList">
|
||||
<t t-if="displayOptions">
|
||||
<ul class="o-autocomplete--dropdown-menu ui-widget show"
|
||||
t-att-class="{
|
||||
'dropdown-menu ui-autocomplete': props.dropdown,
|
||||
'list-group': !props.dropdown
|
||||
}"
|
||||
t-on-mousedown.prevent="" t-ref="sourcesList">
|
||||
<t t-foreach="sources" t-as="source" t-key="source.id">
|
||||
<t t-if="source.isLoading">
|
||||
<li class="o-autocomplete--dropdown-item ui-menu-item">
|
||||
<li class="ui-menu-item"
|
||||
t-att-class="{
|
||||
'o-autocomplete--dropdown-item': props.dropdown,
|
||||
'd-block': !props.dropdown
|
||||
}">
|
||||
<a href="#" class="dropdown-item ui-menu-item-wrapper">
|
||||
<i class="fa fa-spin fa-circle-o-notch" /> <t t-esc="source.placeholder" />
|
||||
</a>
|
||||
@@ -31,7 +40,7 @@
|
||||
<t t-else="">
|
||||
<t t-foreach="source.options" t-as="option" t-key="option.id">
|
||||
<li
|
||||
class="o-autocomplete--dropdown-item ui-menu-item"
|
||||
class="o-autocomplete--dropdown-item ui-menu-item d-block"
|
||||
t-att-class="option.classList"
|
||||
t-on-mouseenter="() => this.onOptionMouseEnter([source_index, option_index])"
|
||||
t-on-mouseleave="() => this.onOptionMouseLeave([source_index, option_index])"
|
||||
|
||||
@@ -11,7 +11,6 @@ import { PopoverController } from "./popover_controller";
|
||||
* popoverClass?: string;
|
||||
* position?: import("@web/core/position_hook").Direction;
|
||||
* onPositioned?: import("@web/core/position_hook").PositionEventHandler;
|
||||
* preventClose?: () => void;
|
||||
* }} PopoverServiceAddOptions
|
||||
*/
|
||||
|
||||
|
||||
+26
-40
@@ -1,6 +1,5 @@
|
||||
/** @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 {
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
Many2ManyTagsField,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { TagsList } from "../many2many_tags/tags_list";
|
||||
import { onMounted, useState } from "@odoo/owl";
|
||||
import { AvatarMany2XAutocomplete } from "@web/views/fields/relational_utils";
|
||||
|
||||
export class Many2ManyTagsAvatarField extends Many2ManyTagsField {
|
||||
@@ -58,30 +56,32 @@ export class Many2ManyTagsAvatarFieldPopover extends Many2ManyTagsAvatarField {
|
||||
static props = {
|
||||
...Many2ManyTagsAvatarField.props,
|
||||
close: { type: Function },
|
||||
deleteTag: { type: Function },
|
||||
updateTag: { type: Function },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({ tags: this.tags });
|
||||
const originalUpdate = this.update;
|
||||
this.update = async (recordList) => {
|
||||
const updatedVal = await this.props.updateTag(recordList);
|
||||
this.state.tags = updatedVal.map((tag) => ({
|
||||
...tag,
|
||||
onDelete: () => this.deleteTag(tag.id),
|
||||
}));
|
||||
await originalUpdate(recordList);
|
||||
await this._saveUpdate();
|
||||
};
|
||||
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),
|
||||
}));
|
||||
|
||||
deleteTag(id) {
|
||||
super.deleteTag(id);
|
||||
this._saveUpdate();
|
||||
}
|
||||
|
||||
async _saveUpdate() {
|
||||
await this.props.record.save({ noReload: true });
|
||||
// manual render to dirty record
|
||||
this.render();
|
||||
// update dropdown
|
||||
this.autoCompleteRef.el.querySelector("input").click();
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return super.tags.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,15 +126,17 @@ export class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {
|
||||
canCreate: false,
|
||||
canCreateEdit: false,
|
||||
canQuickCreate: false,
|
||||
placeholder: this.env._t("Search users..."),
|
||||
},
|
||||
{
|
||||
position: "bottom",
|
||||
popoverClass: "o_m2m_tags_avatar_field_popover",
|
||||
closeOnClickAway: (target) => !target.closest(".modal"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get canDisplayDelete() {
|
||||
return !this.props.readonly && !isMobileOS();
|
||||
get canDisplayQuickAssignAvatar() {
|
||||
return !this.props.readonly && !(this.props.tags && this.otherTags.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,28 +156,12 @@ export class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
|
||||
const props = {
|
||||
...this.props,
|
||||
readonly: false,
|
||||
deleteTag: this.deleteTag.bind(this),
|
||||
updateTag: this.updateTag.bind(this),
|
||||
};
|
||||
delete props.isEditable;
|
||||
return props;
|
||||
}
|
||||
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),
|
||||
onDelete: () => this.deleteTag(record.id),
|
||||
};
|
||||
get tags() {
|
||||
return super.tags.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-11
@@ -81,20 +81,26 @@
|
||||
}
|
||||
}
|
||||
.o_m2m_tags_avatar_field_popover {
|
||||
max-width: 280px;
|
||||
z-index: $zindex-modal - 1;
|
||||
width: 280px;
|
||||
font-size: $dropdown-font-size;
|
||||
.o_input.o_tags_input {
|
||||
border: none;
|
||||
}
|
||||
.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-autocomplete--dropdown-menu {
|
||||
font-size: $dropdown-font-size;
|
||||
.dropdown-item {
|
||||
padding: $o-dropdown-vpadding $o-dropdown-hpadding/2;
|
||||
}
|
||||
}
|
||||
.o_allow_delete_hover:hover .o_delete {
|
||||
visibility: visible;
|
||||
.o-autocomplete .o-autocomplete--input.o_input {
|
||||
border: 1px solid var(--o-input-border-color);
|
||||
border-width: 0 0 1px 0;
|
||||
margin-bottom: .5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
background-color: var(--o-input-background-color, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-15
@@ -26,21 +26,15 @@
|
||||
</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 expr="//t[@t-foreach='visibleTags']" position="before">
|
||||
<t t-if="canDisplayQuickAssignAvatar">
|
||||
<a t-on-click.stop.prevent="openPopover" tabIndex="-1" href="#" title="Assign"
|
||||
aria-label="Assign" class="fa fa-user-plus o_quick_assign o_m2m_avatar d-flex align-items-center"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_m2m_avatar_empty')]" position="attributes">
|
||||
<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"
|
||||
@@ -52,14 +46,16 @@
|
||||
</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=" " />
|
||||
<attribute name="class" add="p-2 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>
|
||||
<Many2XAutocomplete position="attributes">
|
||||
<attribute name="dropdown">false</attribute>
|
||||
<attribute name="placeholder">props.placeholder</attribute>
|
||||
<attribute name="autofocus">true</attribute>
|
||||
</Many2XAutocomplete>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { isMobileOS } from "@web/core/browser/feature_detection";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { many2OneField, Many2OneField } from "../many2one/many2one_field";
|
||||
|
||||
import { Component, onMounted } from "@odoo/owl";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { AvatarMany2XAutocomplete } from "@web/views/fields/relational_utils";
|
||||
|
||||
export class Many2OneAvatarField extends Component {
|
||||
@@ -41,9 +40,12 @@ export class Many2OneFieldPopover extends Many2OneField {
|
||||
static components = {
|
||||
Many2XAutocomplete: AvatarMany2XAutocomplete,
|
||||
};
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => this.focusInput());
|
||||
get Many2XAutocompleteProps() {
|
||||
return {
|
||||
...super.Many2XAutocompleteProps,
|
||||
dropdown: false,
|
||||
autofocus: true,
|
||||
};
|
||||
}
|
||||
|
||||
async updateRecord(value) {
|
||||
@@ -91,23 +93,15 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
|
||||
canCreate: false,
|
||||
canCreateEdit: false,
|
||||
canQuickCreate: false,
|
||||
placeholder: this.env._t("Search user..."),
|
||||
},
|
||||
{
|
||||
position: "bottom",
|
||||
popoverClass: "o_m2o_tags_avatar_field_popover",
|
||||
closeOnClickAway: (target) => !target.closest(".modal"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get canDisplayDelete() {
|
||||
return this.props.isEditable && this.props.record.data[this.props.name] && !isMobileOS();
|
||||
}
|
||||
async remove(ev) {
|
||||
if (!this.props.isEditable) {
|
||||
return;
|
||||
}
|
||||
await this.props.record.update({ [this.props.name]: false });
|
||||
await this.props.record.save();
|
||||
}
|
||||
}
|
||||
|
||||
export const kanbanMany2OneAvatarField = {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
.o_popover > .o_field_many2one_selection {
|
||||
padding: 0.25rem;
|
||||
.o-autocomplete--dropdown-menu {
|
||||
width: 178px;
|
||||
}
|
||||
}
|
||||
.o_field_many2one_avatar_kanban {
|
||||
.o_m2o_avatar {
|
||||
.o_delete {
|
||||
margin-left: -12px;
|
||||
margin-top: -4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
visibility: hidden;
|
||||
.o_m2o_tags_avatar_field_popover {
|
||||
z-index: $zindex-modal - 1;
|
||||
font-size: $dropdown-font-size;
|
||||
> .o_field_many2one_selection {
|
||||
padding: 0.25rem;
|
||||
.o-autocomplete--dropdown-menu {
|
||||
width: 178px;
|
||||
font-size: $dropdown-font-size;
|
||||
.dropdown-item {
|
||||
padding: $o-dropdown-vpadding $o-dropdown-hpadding/2;
|
||||
}
|
||||
}
|
||||
&.o_allow_delete_hover:hover .o_delete {
|
||||
visibility: visible;
|
||||
.o-autocomplete--input {
|
||||
margin-bottom: .5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,12 @@
|
||||
<t t-name="web.KanbanMany2OneAvatarField" t-inherit="web.Many2OneAvatarField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar')]" position="attributes">
|
||||
<attribute name="class" separator=" " add="d-flex"/>
|
||||
<attribute name="t-att-class">{'o_allow_delete_hover': canDisplayDelete}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar')]//img" position="after">
|
||||
<a tabIndex="-1" href="#" title="Remove" aria-label="Remove" t-on-click.stop.prevent="remove" class="fa
|
||||
fa-times-circle fs-6 text-danger bg-white align-self-start z-index-1 pt-0 rounded-circle o_delete" />
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar_empty')]" position="replace">
|
||||
<img t-if="props.record.data[props.name] === false" title="Quick assign" alt="Quick assign"
|
||||
src="/web/static/img/user_menu_avatar.png" t-on-click.stop.prevent="openPopover"
|
||||
class="rounded-circle align-self-center "/>
|
||||
<t t-if="props.record.data[props.name] === false">
|
||||
<a t-on-click.stop.prevent="openPopover" tabIndex="-1" href="#" title="Assign"
|
||||
aria-label="Assign" class="fa fa-user-plus o_quick_assign d-flex align-items-center"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -384,6 +384,8 @@ Many2XAutocomplete.props = {
|
||||
autoSelect: { type: Boolean, optional: true },
|
||||
isToMany: { type: Boolean, optional: true },
|
||||
autocomplete_container: { type: Function, optional: true },
|
||||
dropdown: { type: Boolean, optional: true },
|
||||
autofocus: { type: Boolean, optional: true },
|
||||
};
|
||||
Many2XAutocomplete.defaultProps = {
|
||||
searchLimit: 7,
|
||||
@@ -393,6 +395,7 @@ Many2XAutocomplete.defaultProps = {
|
||||
setInputFloats: () => {},
|
||||
quickCreate: null,
|
||||
context: {},
|
||||
dropdown: true,
|
||||
};
|
||||
|
||||
export class AvatarMany2XAutocomplete extends Many2XAutocomplete {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<t t-name="web.Many2XAutocomplete" owl="1" >
|
||||
<div class="o_input_dropdown" t-ref="autocomplete_container">
|
||||
<input t-if="env.isSmall"
|
||||
<input t-if="env.isSmall and props.dropdown"
|
||||
type="text"
|
||||
t-att-id="props.id"
|
||||
class="o_input"
|
||||
@@ -52,6 +52,8 @@
|
||||
onSelect.bind="onSelect"
|
||||
onInput.bind="onInput"
|
||||
onChange.bind="onChange"
|
||||
dropdown="props.dropdown"
|
||||
autofocus="props.autofocus"
|
||||
/>
|
||||
<a role="button" class="o_dropdown_button" draggable="false" />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
getFixture,
|
||||
triggerEvent,
|
||||
selectDropdownItem,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { triggerHotkey } from "../../helpers/utils";
|
||||
|
||||
@@ -279,16 +273,14 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
|
||||
assert.expect(17);
|
||||
assert.expect(21);
|
||||
|
||||
const records = [];
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
records.push({
|
||||
serverData.models.partner.records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner.records = serverData.models.partner.records.concat(records);
|
||||
|
||||
serverData.models.turtle.records.push({
|
||||
id: 4,
|
||||
@@ -300,6 +292,8 @@ QUnit.module("Fields", (hooks) => {
|
||||
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5];
|
||||
serverData.views = {
|
||||
"turtle,false,form": '<form><field name="display_name"/></form>',
|
||||
"partner,false,list": '<tree><field name="display_name"/></tree>',
|
||||
"partner,false,search": "<search/>",
|
||||
};
|
||||
|
||||
await makeView({
|
||||
@@ -331,12 +325,10 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:first-child .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_quick_assign",
|
||||
"should have the assign icon"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
@@ -355,14 +347,14 @@ QUnit.module("Fields", (hooks) => {
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"/web/image/partner/5/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
)[1].dataset.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"/web/image/partner/4/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.containsOnce(
|
||||
@@ -402,42 +394,49 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
assert.containsNone(target, ".o_field_many2many_tags_avatar .o_field_many2many_selection");
|
||||
|
||||
// check delete
|
||||
const firstTag = target.querySelector(
|
||||
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag"
|
||||
);
|
||||
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"
|
||||
);
|
||||
|
||||
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);
|
||||
await click(o_kanban_record, ".o_field_tags > .o_m2m_avatar_empty", true);
|
||||
const popover = document.querySelector(".o-overlay-container");
|
||||
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tags");
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
popover.querySelector("input"),
|
||||
"the input inside the popover should have the focus"
|
||||
);
|
||||
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 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(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tag");
|
||||
assert.strictEqual(
|
||||
o_kanban_record.querySelectorAll(".o_tag").length,
|
||||
2,
|
||||
"Should have 2 tags"
|
||||
);
|
||||
await click(target);
|
||||
// select first input
|
||||
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
|
||||
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 tags");
|
||||
assert.strictEqual(
|
||||
o_kanban_record.querySelectorAll(".o_tag").length,
|
||||
2,
|
||||
"Should have 2 tags"
|
||||
);
|
||||
// load more
|
||||
await click(popover.querySelector(".o_m2o_dropdown_option_search_more"));
|
||||
// first item
|
||||
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
|
||||
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 4, "Should have 4 tags");
|
||||
assert.strictEqual(
|
||||
o_kanban_record.querySelectorAll(".o_tag").length,
|
||||
2,
|
||||
"Should have 2 tags"
|
||||
);
|
||||
assert.strictEqual(
|
||||
o_kanban_record.querySelector("img.o_m2m_avatar").dataset.src,
|
||||
"/web/image/partner/4/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
await click(
|
||||
target.querySelector(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {
|
||||
|
||||
@@ -339,8 +339,65 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.containsOnce(target, ".o_field_widget[name=user_id] span.o_m2o_avatar_empty");
|
||||
});
|
||||
|
||||
QUnit.test("widget many2one_avatar in kanban view (load more dialog)", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
for (let id = 1; id <= 10; id++) {
|
||||
serverData.models.user.records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
serverData.views = {
|
||||
"user,false,list": '<tree><field name="display_name"/></tree>',
|
||||
"user,false,search": "<search/>",
|
||||
};
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="oe_kanban_footer">
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
// open popover
|
||||
await click(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > a.o_quick_assign"
|
||||
)
|
||||
);
|
||||
|
||||
// load more
|
||||
await click(
|
||||
document.querySelector(".o-overlay-container .o_m2o_dropdown_option_search_more")
|
||||
);
|
||||
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).dataset.src,
|
||||
"/web/image/user/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2one_avatar in kanban view", async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(5);
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
@@ -370,22 +427,23 @@ QUnit.module("Fields", (hooks) => {
|
||||
"/web/image/user/17/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).dataset.src,
|
||||
"/web/static/img/user_menu_avatar.png",
|
||||
"should have empty avatar image"
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
|
||||
"should have the quick assign icon"
|
||||
);
|
||||
// open dialog
|
||||
// open popover
|
||||
await click(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
)
|
||||
);
|
||||
// select input
|
||||
const popover = document.querySelector(".o-overlay-container");
|
||||
await click(popover.querySelector(".o_field_many2one_selection input"));
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
popover.querySelector("input"),
|
||||
"the input inside the popover should have the focus"
|
||||
);
|
||||
// select first input
|
||||
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
|
||||
assert.strictEqual(
|
||||
@@ -395,22 +453,10 @@ QUnit.module("Fields", (hooks) => {
|
||||
"/web/image/user/17/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
// check delete
|
||||
await triggerEvent(
|
||||
target.querySelector(".o_kanban_record:nth-child(4) .o_field_many2one_avatar"),
|
||||
null,
|
||||
"mouseover"
|
||||
);
|
||||
await click(
|
||||
target.querySelector(".o_kanban_record:nth-child(4) .o_field_many2one_avatar"),
|
||||
".o_delete"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).dataset.src,
|
||||
"/web/static/img/user_menu_avatar.png",
|
||||
"should have empty avatar image"
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
|
||||
"should not have the quick assign icon"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user