[FIX] web: restore correct m2m_tags style

This commit completes the style of the tags of m2m_tags field.
Tags did not have correct colors and had border.
The commit also fixes the m2m_tags_avatar field for the same reason
as well as a traceback when clicking on the remove tag button.

closes odoo/odoo#96654

Related: odoo/enterprise#30012
Signed-off-by: Bruno Boi (boi) <boi@odoo.com>
This commit is contained in:
Bruno Boi
2022-08-18 15:07:43 +02:00
committed by Michael (mcm)
parent 209cf112f1
commit df05f8f91d
10 changed files with 315 additions and 107 deletions
@@ -174,8 +174,8 @@ export class Many2ManyTagsField extends Component {
}
focusTag(index) {
const tagListEl = this.autoCompleteRef.el.previousElementSibling;
const tags = tagListEl.getElementsByClassName("badge");
const autoCompleteParent = this.autoCompleteRef.el.parentElement;
const tags = autoCompleteParent.getElementsByClassName("badge");
if (tags.length) {
if (index === undefined) {
tags[tags.length - 1].focus();
@@ -235,8 +235,8 @@ export class Many2ManyTagsField extends Component {
return;
}
const hotkey = getActiveHotkey(ev);
const tagListEl = this.autoCompleteRef.el.previousElementSibling;
const tags = [...tagListEl.getElementsByClassName("badge")];
const autoCompleteParent = this.autoCompleteRef.el.parentElement;
const tags = [...autoCompleteParent.getElementsByClassName("badge")];
const closestTag = ev.target.closest(".badge");
const tagIndex = tags.indexOf(closestTag);
const input = this.autoCompleteRef.el.querySelector(".o-autocomplete--input");
@@ -6,10 +6,75 @@
line-height: $o-line-height-base;
}
.o_field_widget.o_field_many2many_tags .o_input {
height: 100%;
.o_field_widget.o_field_many2many_tags {
flex-flow: row wrap;
.o_tags_input {
padding: 1px 0;
.o_tag {
padding-left: 0.6em;
padding-right: 0.6em;
}
}
.o_field_many2many_selection {
flex: 1 0 100px;
.o_input {
height: 100%;
border: none;
}
}
.badge {
flex: 0 0 auto;
margin: 1px 2px 1px 0;
border: none;
font-size: 12px;
user-select: none;
display: flex;
max-width: 100%;
&.dropdown {
cursor: pointer;
}
a {
color: inherit;
}
.o_badge_text, .o_tag_badge_text {
@include o-text-overflow(inline-block);
max-width: 200px;
color: inherit;
line-height: 1.1;
}
.o_delete {
color: inherit;
cursor: pointer;
padding-left: 4px;
line-height: 1;
}
}
}
.o_list_view .o_field_widget.o_field_many2many_tags .o_field_many2many_selection {
flex-basis: 40px;
.o_list_view .o_field_widget.o_field_many2many_tags {
.o_tags_input {
border: 0;
padding: 0;
}
.o_field_many2many_selection {
flex-basis: 40px;
}
}
.o_form_view .o_group .o_field_tags {
width: 100%;
}
.o_form_statusbar .o_field_tags {
align-self: center;
}
@@ -2,22 +2,27 @@
<templates xml:space="preserve">
<t t-name="web.Many2ManyTagsField" owl="1">
<TagsList tags="tags" t-on-keydown="onTagsKeydown"/>
<div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-block mw-100" t-on-keydown="onAutoCompleteKeydown" t-ref="autoComplete">
<Many2XAutocomplete
id="props.id"
placeholder="props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="context"
getDomain.bind="getDomain"
isToMany="true"
nameCreateField="props.nameCreateField"
/>
<div
class="o_field_tags d-inline-flex flex-wrap mw-100"
t-att-class="{'o_tags_input o_input': !props.readonly}"
>
<TagsList tags="tags" t-on-keydown="onTagsKeydown"/>
<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"
placeholder="props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="context"
getDomain.bind="getDomain"
isToMany="true"
nameCreateField="props.nameCreateField"
/>
</div>
</div>
</t>
@@ -1,49 +1,66 @@
.o_tags_list {
gap: 2px;
.o_tag {
user-select: none;
.o_tag {
user-select: none;
.o_badge_text, a {
color: inherit;
.o_badge_text, a {
color: inherit;
line-height: 1.1;
}
.o_tag_badge_text {
font-size: 12px;
}
img {
height: 1.6em;
aspect-ratio: 1/1;
+ .o_tag_badge_text {
padding: 0.25em 0;
}
.o_tag_badge_text {
font-size: 12px;
}
img {
height: 1.6em;
aspect-ratio: 1/1;
+ .o_tag_badge_text {
padding: 0.25em 0;
}
}
@for $size from 1 through length($o-colors) {
&.o_tag_color_#{$size - 1} {
@if $size == 1 {
& {
background-color: $o-view-background-color;
color: nth($o-colors, $size);
}
&:focus-within {
color: darken(nth($o-colors, $size), 40%) ;
}
&::after {
background-color: nth($o-colors, $size);
}
} @else {
&, &::after {
background-color: nth($o-colors, $size);
color: color-contrast(nth($o-colors, $size));
}
&:focus-within {
background-color: darken(nth($o-colors, $size), 20%);
}
}
@for $size from 1 through length($o-colors) {
&.o_tag_color_#{$size - 1} {
@if $size == 1 {
& {
background-color: $o-view-background-color;
color: nth($o-colors, $size);
box-shadow: inset 0 0 0 1px;
}
&:focus-within {
color: darken(nth($o-colors, $size), 40%) ;
}
&::after {
background-color: nth($o-colors, $size);
}
} @else {
&, &::after {
background-color: nth($o-colors, $size);
color: color-contrast(nth($o-colors, $size));
}
&:focus-within {
background-color: darken(nth($o-colors, $size), 20%);
}
}
}
}
}
}
.o_kanban_view .o_kanban_record .o_tag {
display: inline-block;
margin-right: 4px;
font-size: 11px;
font-weight: 500;
background-color: transparent;
color: inherit;
box-shadow: none;
@include o-kanban-tag-color;
span {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 100%;
}
}
@@ -2,22 +2,20 @@
<templates xml:space="preserve">
<t t-name="web.TagsList" owl="1">
<span t-if="visibleTags.length" t-attf-class="o_tags_list {{props.className}} d-inline-flex flex-wrap" t-att-name="props.name">
<t t-if="props.tags" t-foreach="visibleTags" t-as="tag" t-key="tag.id or tag_index">
<span t-attf-class="{{`o_tag_color_${ tag.colorIndex or 0 } ${props.displayBadge ? 'badge rounded-pill' : ''}`}} o_tag d-inline-flex align-items-center h-100" tabindex="-1" t-att-data-color="tag.colorIndex" t-att-title="tag.text" t-on-click="(ev) => tag.onClick and tag.onClick(ev)">
<img t-if="tag.img" t-att-src="tag.img" class="o_m2m_avatar rounded-circle"/>
<div t-if="props.displayBadge" class="o_tag_badge_text" t-esc="tag.text" />
<t t-else="">
<span />
<div t-esc="tag.text" />
</t>
<a tabIndex="-1" t-if="tag.onDelete" t-on-click.stop.prevent="(ev) => tag.onDelete and tag.onDelete(ev)" href="#" class="fa fa-times o_delete ms-2" title="Delete" aria-label="Delete"></a>
</span>
</t>
<span t-if="props.tags and otherTags.length" class="o_m2m_avatar_empty rounded-circle text-center fw-bold" t-att-data-tooltip="tooltip">
<span t-if="otherTags.length > 9" t-esc="'9+'" />
<span t-else="" t-esc="'+' + otherTags.length" />
<t t-foreach="visibleTags" t-as="tag" t-key="tag.id or tag_index">
<span t-attf-class="{{`o_tag_color_${ tag.colorIndex or 0 } ${props.displayBadge ? 'badge rounded-pill' : ''}`}} o_tag d-inline-flex align-items-center h-100" tabindex="-1" t-att-data-color="tag.colorIndex" t-att-title="tag.text" t-on-click="(ev) => tag.onClick and tag.onClick(ev)">
<img t-if="tag.img" t-att-src="tag.img" class="o_m2m_avatar rounded-circle"/>
<div t-if="props.displayBadge" class="o_tag_badge_text" t-esc="tag.text" />
<t t-else="">
<span />
<div t-esc="tag.text" />
</t>
<a tabIndex="-1" t-if="tag.onDelete" t-on-click.stop.prevent="(ev) => tag.onDelete and tag.onDelete(ev)" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"></a>
</span>
</t>
<span t-if="props.tags and otherTags.length" class="o_m2m_avatar_empty rounded-circle text-center fw-bold" t-att-data-tooltip="tooltip">
<span t-if="otherTags.length > 9" t-esc="'9+'" />
<span t-else="" t-esc="'+' + otherTags.length" />
</span>
</t>
@@ -11,7 +11,7 @@ export class Many2ManyTagsAvatarField extends Many2ManyTagsField {
id: record.id, // datapoint_X
text: record.data.display_name,
img: `/web/image/${this.props.relation}/${record.resId}/avatar_128`,
onDelete: !this.props.readonly ? () => this.onDelete(record.resId) : undefined,
onDelete: !this.props.readonly ? () => this.deleteTag(record.id) : undefined,
}));
}
}
@@ -1,9 +1,82 @@
.o_field_many2many_tags_avatar {
.o_tag {
padding: 0;
.o_field_widget.o_field_many2many_tags_avatar {
flex-flow: row wrap;
.o_tags_input {
padding: 1px 0;
}
.o_field_many2many_selection {
flex: 1 0 100px;
.o_input {
height: 100%;
border: none;
}
}
.o_dropdown_button {
top: $o-input-padding-y;
}
.badge {
border: 0;
flex: 0 0 auto;
margin: 1px 2px 1px 0;
border: none;
font-size: 12px;
user-select: none;
display: flex;
max-width: 100%;
align-items: center;
padding: 0;
padding-right: 0.6rem;
box-shadow: inset 0 0 0 1px;
&.dropdown {
cursor: pointer;
}
a {
color: inherit;
}
.o_badge_text {
@include o-text-overflow(inline-block);
max-width: 200px;
color: inherit;
line-height: 1.1;
}
.o_delete {
color: inherit;
cursor: pointer;
padding-left: 4px;
line-height: 1;
}
img {
height: 1.4em;
width: 1.4em;
object-fit: cover;
}
.o_badge_text, .o_delete {
padding-top: 0.25em;
padding-bottom: 0.25em;
}
.o_tag_badge_text {
padding-left:2px;
}
}
}
.o_list_view .o_field_widget.o_field_many2many_tags_avatar {
.o_tags_input {
border: 0;
padding: 0;
}
.o_field_many2many_selection {
flex-basis: 40px;
}
}
@@ -2,21 +2,26 @@
<templates xml:space="preserve">
<t t-name="web.Many2ManyTagsAvatarField" owl="1">
<TagsList tags="tags" itemsVisible="itemsVisible" t-on-keydown="onTagsKeydown"/>
<div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-block mw-100" t-on-keydown="onAutoCompleteKeydown" t-ref="autoComplete">
<Many2XAutocomplete
id="props.id"
placeholder="props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="props.string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="context"
getDomain.bind="getDomain"
isToMany="true"
/>
<div
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="itemsVisible" t-on-keydown="onTagsKeydown"/>
<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"
placeholder="props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="props.string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="context"
getDomain.bind="getDomain"
isToMany="true"
/>
</div>
</div>
</t>
@@ -1,6 +1,12 @@
/** @odoo-module **/
import { click, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
import {
click,
clickEdit,
clickSave,
getFixture,
selectDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@@ -290,7 +296,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsN(
target,
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tags_list > span:not(.o_m2m_avatar_empty)",
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tag",
2,
"should have 2 records"
);
@@ -325,7 +331,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tags_list > span:not(.o_m2m_avatar_empty)",
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tag",
2,
"should have 2 records"
);
@@ -358,4 +364,43 @@ QUnit.module("Fields", (hooks) => {
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) {
await makeView({
type: "form",
resModel: "turtle",
resId: 2,
serverData,
arch: `
<form>
<sheet>
<field name="partner_ids" widget="many2many_tags_avatar"/>
</sheet>
</form>`,
});
await clickEdit(target);
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
2,
"should have 2 records"
);
await click(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
"should have 1 record"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
"should have 1 record"
);
});
});
@@ -5947,7 +5947,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
".o_field_many2many_tags .o_input",
".o_field_many2many_tags .o_tags_input",
"should display the line in editable mode"
);
assert.strictEqual(
@@ -6000,7 +6000,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
".o_field_many2many_tags .o_input",
".o_field_many2many_tags .o_tags_input",
"should display the line in editable mode"
);
assert.strictEqual(