[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:
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user