[FIX] web,*: kanban: support t-set defined outside cards

*base, event, hr_holidays

Before this commit, `<t t-set/>` nodes declared outside the main
div of a kanban card where ignored by the kanban compiler. This
commit fixes that issue. To do so, we refactor a bit the way the
compiler and KanbanRecord handle multiple root cards (typically,
several roots with a t-if/t-elif/t-else, s.t. there's a unique
rendered root), by removing the faulty logic from the compiler
that identified card root nodes (and filtered out t-set nodes),
and introducing a div for the KanbanRecord component on which we
can set classNames, attributes and handlers.

The issue could be observed on the Apps kanban view, as it
displayed the "Install" button, whether the app was already
installed or not.

closes odoo/odoo#95211

Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Aaron Bohy
2022-07-05 11:35:16 +02:00
parent 61f54e5b88
commit e37a663b52
9 changed files with 62 additions and 65 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
.o_kanban_view .o_event_kanban_view {
.o_kanban_record {
.o_kanban_record > div {
min-height: 140px;
}
.o_kanban_content {
@@ -8,8 +8,7 @@ import {
getTag,
} from "@web/core/utils/xml";
import { toStringExpression } from "@web/views/utils";
import { isTextNode, ViewCompiler } from "@web/views/view_compiler";
import { KANBAN_BOX_ATTRIBUTE, KANBAN_TOOLTIP_ATTRIBUTE } from "./kanban_arch_parser";
import { ViewCompiler } from "@web/views/view_compiler";
/**
* @typedef {Object} DropdownDef
@@ -24,20 +23,11 @@ const INTERP_REGEXP = /\{\{.*?\}\}|#\{.*?\}/g;
const ACTION_TYPES = ["action", "object"];
const SPECIAL_TYPES = [...ACTION_TYPES, "edit", "open", "delete", "url", "set_cover"];
function getValidCards(...cardEls) {
return cardEls.map((c) => (isValidCard(c) ? [c] : getValidCards(...c.childNodes)));
}
function isValidCard(el) {
return getTag(el, true) !== "t" || el.hasAttribute("t-component");
}
let currentDropdownId = 1;
export class KanbanCompiler extends ViewCompiler {
setup() {
this.ctx.readonly = "read_only_mode";
this.compilers.push(
{ selector: `[t-name='${KANBAN_BOX_ATTRIBUTE}']`, fn: this.compileCard },
{ selector: ".oe_kanban_colorpicker", fn: this.compileColorPicker },
{
selector: ".dropdown,.o_kanban_manage_button_section",
@@ -172,40 +162,6 @@ export class KanbanCompiler extends ViewCompiler {
return compiled;
}
/**
* @param {Element} el
* @param {Object} params
* @returns {Element}
*/
compileCard(el, params) {
const compiledCard = this.compileGenericNode(el, params);
const cards = getValidCards(compiledCard)
.flat(Infinity)
.filter((card) => card && !isTextNode(card));
// Add a root ref if we need to use it to add tooltips on the cards
// This means that multi-root kanban-box templates cannot have tooltips
// WOWL FIXME: always define ref post merge and expose whether the kanban
// is grouped or not so that kanban-box can use t-if t-else to avoid
// multi-root scenarios in "kanban list" templates.
const shouldDefineRef =
el.ownerDocument.querySelector(`[t-name='${KANBAN_TOOLTIP_ATTRIBUTE}']`) !== null;
for (const card of cards) {
card.setAttribute("t-att-tabindex", `props.record.model.useSampleModel?-1:0`);
card.setAttribute("role", `article`);
card.setAttribute("t-att-data-id", `props.canResequence and props.record.id`);
card.setAttribute("t-on-click", `onGlobalClick`);
if (shouldDefineRef) {
card.setAttribute("t-ref", "root");
}
combineAttributes(card, "t-att-class", "getRecordClasses()", "+' '+");
}
return createElement("t", cards);
}
/**
* @returns {Element}
*/
@@ -11,7 +11,7 @@
padding: 0px!important;
}
.o_kanban_record, .o_kanban_quick_create {
.o_kanban_record > div, .o_kanban_quick_create {
padding: $o-kanban-inside-vgutter $o-kanban-inside-hgutter;
border: 1px solid $border-color;
background-color: $o-view-background-color;
@@ -52,9 +52,13 @@
min-width: 150px;
margin: 0 0 -1px;
> div {
height: 100%;
}
// ------- Kanban Record, v11 Layout -------
// Records colours
&::after {
> div::after {
content: "";
@include o-position-absolute(0, auto, 0, 0);
width: $o-kanban-color-border-width;
@@ -155,7 +159,7 @@
// ---------- Kanban Record, fill image design ----------
// Records with images that compensate record's padding
// filling all the available space (eg. hr, partners.. )
&.o_kanban_record_has_image_fill {
.o_kanban_record_has_image_fill {
display: flex;
.o_kanban_image_fill_left {
@@ -228,6 +232,8 @@
.o_dragged {
@extend .shadow;
transform: rotate(-3deg);
transition: transform 0.6s, box-shadow 0.3s;
}
// ------- Compatibility of old (<= v10) Generic layouts -------
@@ -331,7 +337,8 @@
}
// Kanban Record - Utility classes
&.oe_kanban_global_click, &.oe_kanban_global_click_edit {
&.oe_kanban_global_click, &.oe_kanban_global_click_edit,
.oe_kanban_global_click, .oe_kanban_global_click_edit {
cursor: pointer;
&:focus, &:focus-within {
outline: thin solid mix(theme-color('primary'), gray('400'));
@@ -339,11 +346,6 @@
}
}
&.o_dragged {
transform: rotate(-3deg);
transition: transform 0.6s, box-shadow 0.3s;
}
.o_attachment_image > img {
width: 100%;
height: auto;
@@ -1,10 +1,10 @@
.o_kanban_dashboard {
.o_kanban_dashboard:not(.o_legacy_kanban_view) {
&:not(.o_kanban_grouped) {
// correctly display the no_content_helper in dashboards
flex-flow: row wrap;
}
.o_kanban_record {
.o_kanban_record > div {
position: relative;
display: flex;
flex-flow: column nowrap;
@@ -19,7 +19,7 @@ import { KANBAN_BOX_ATTRIBUTE, KANBAN_TOOLTIP_ATTRIBUTE } from "./kanban_arch_pa
import { KanbanCompiler } from "./kanban_compiler";
import { KanbanCoverImageDialog } from "./kanban_cover_image_dialog";
const { Component, onWillUpdateProps, xml } = owl;
const { Component, onMounted, onWillUpdateProps, useRef, xml } = owl;
const { COLORS } = ColorList;
const formatters = registry.category("formatters");
@@ -169,6 +169,11 @@ export class KanbanRecord extends Component {
}
this.createRecordAndWidget(this.props);
this.rootRef = useRef("root");
onMounted(() => {
// FIXME: this needs to be changed to an attribute on the root node...
this.allowGlobalClick = !!this.rootRef.el.querySelector(ALLOW_GLOBAL_CLICK);
});
onWillUpdateProps(this.createRecordAndWidget);
}
@@ -252,7 +257,7 @@ export class KanbanRecord extends Component {
record.model.notify();
},
});
} else if (forceGlobalClick || ev.target.closest(ALLOW_GLOBAL_CLICK)) {
} else if (forceGlobalClick || this.allowGlobalClick) {
openRecord(record);
}
}
@@ -393,4 +398,13 @@ KanbanRecord.props = [
"record",
"templates",
];
KanbanRecord.template = xml`<t t-call="{{ templates['${KANBAN_BOX_ATTRIBUTE}'] }}" />`;
KanbanRecord.template = xml`
<div
role="article"
t-att-class="getRecordClasses()"
t-att-data-id="props.canResequence and props.record.id"
t-att-tabindex="props.record.model.useSampleModel ? -1 : 0"
t-on-click="onGlobalClick"
t-ref="root">
<t t-call="{{ templates['${KANBAN_BOX_ATTRIBUTE}'] }}"/>
</div>`;
@@ -339,7 +339,7 @@ QUnit.module("Fields", (hooks) => {
});
assert.containsOnce(target, `img[data-src="data:image/png;base64,${MY_IMAGE}"]`);
assert.containsOnce(target, ".o_kanban_record.oe_kanban_global_click");
assert.containsOnce(target, ".o_kanban_record .oe_kanban_global_click");
// Actual flow: click on an element of the m2m to get its form view
await click(target, ".oe_kanban_global_click");
@@ -145,7 +145,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsOnce(
target,
".o_kanban_record.oe_kanban_global_click",
".o_kanban_record .oe_kanban_global_click",
"There should be one record in the many2many"
);
@@ -602,7 +602,7 @@ QUnit.module("Views", (hooks) => {
[...target.querySelectorAll(".o_kanban_group")].map((el) =>
el.innerText.replace(/\s/g, " ")
),
["None (1)", "gold yopblip", "silver yopgnap"]
["None (1)", "gold yop blip", "silver yop gnap"]
);
await click(getColumn(0));
@@ -707,6 +707,31 @@ QUnit.module("Views", (hooks) => {
);
});
QUnit.test("kanban with t-set outside card", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="int_field"/>
<templates>
<t t-name="kanban-box">
<t t-set="x" t-value="record.int_field.value"/>
<div>
<t t-esc="x"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")),
["10", "9", "17", "-4"]
);
});
QUnit.test("kanban with t-if/t-else on field", async (assert) => {
await makeView({
type: "kanban",
@@ -3537,7 +3562,7 @@ QUnit.module("Views", (hooks) => {
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4);
await click(target, ".oe_kanban_global_click:first-child .o_field_monetary[name=salary]");
await click(target.querySelector(".oe_kanban_global_click .o_field_monetary[name=salary]"));
});
QUnit.test("o2m loaded in only one batch", async (assert) => {
+1 -1
View File
@@ -12,7 +12,7 @@
width: 77%;
}
.o_kanban_view .oe_module_vignette.o_kanban_record {
.o_kanban_view .o_modules_kanban .oe_module_vignette {
align-items: center;
display: flex;
}