[FIX] web: Support multiple dropdowns in kanban

This commit allows multiple declarations of a dropdown menu in a kanban
card to generate a dropdown component for each definition.

Part-of: odoo/odoo#92475
This commit is contained in:
Julien Mougenot
2022-06-30 09:29:49 +02:00
committed by Aaron Bohy
parent 6cdd91b614
commit 2b7f497442
2 changed files with 187 additions and 75 deletions
@@ -11,6 +11,14 @@ 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";
/**
* @typedef {Object} DropdownDef
* @property {Element} el
* @property {boolean} inserted
* @property {boolean} shouldInsert
* @property {("dropdown" | "toggler" | "menu")[]} parts
*/
const INTERP_REGEXP = /\{\{.*?\}\}|#\{.*?\}/g;
const ACTION_TYPES = ["action", "object"];
@@ -24,6 +32,7 @@ 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";
@@ -35,37 +44,66 @@ export class KanbanCompiler extends ViewCompiler {
fn: this.compileDropdown,
doNotCopyAttributes: true,
},
{
selector: ".dropdown-toggle,.o_kanban_manage_toggle_button",
fn: this.compileDropdownButton,
doNotCopyAttributes: true,
},
{
selector: ".dropdown-menu",
fn: this.compileDropdownMenu,
doNotCopyAttributes: true,
},
{
selector: ".dropdown-toggle,.o_kanban_manage_toggle_button",
fn: this.compileDropdownToggler,
doNotCopyAttributes: true,
},
{ selector: "t[t-call]", fn: this.compileTCall }
);
this.dropdown = null;
/** @type {Record<number, DropdownDef>} */
this.dropdowns = {};
}
/**
* Renders a Dropdown component node, or nothing if one has already been rendered.
* This means that the first method to define a dropdown, dropdown toggler or
* dropdown menu will be the place the dropdown component will be inserted.
* Renders a Dropdown component node definition object. It works as follows:
*
* @returns {Element | null}
* The first time a node defining a dropdown component or component slot (dropdown
* root, toggler or default=menu), a new dropdown object will be generated,
* taking note of what parts have already been defined.
*
* It also creates the Dropdown component node that will be inserted in the
* template, at the position of the first node defining either the root dropdown
* or the dropdown toggler.
*
* Each next component or slot still not defined on the current dropdown object
* will be added to it.
*
* If a duplicate part or component is found, a new dropdown object is created
* and set as the new current dropdown.
*
* @param {"dropdown" | "toggler" | "menu"} part
* @returns {DropdownDef}
*/
renderDropdown() {
if (this.dropdown) {
return null;
renderDropdown(part) {
if (!this.dropdowns[currentDropdownId]) {
this.dropdowns[currentDropdownId] = {
parts: [],
inserted: false,
shouldInsert: false,
el: createElement("Dropdown", {
class: toStringExpression("o_dropdown_kanban"),
position: toStringExpression("bottom-end"),
}),
};
}
this.dropdown = createElement("Dropdown", {
position: toStringExpression("bottom-end"),
});
this.dropdown.setAttribute("class", "'o_dropdown_kanban'");
return this.dropdown;
const dropdown = this.dropdowns[currentDropdownId];
if (dropdown.parts.includes(part)) {
// Duplicate part: generate new dropdown
currentDropdownId++;
return this.renderDropdown(part);
}
dropdown.parts.push(part);
if (part !== "menu") {
dropdown.shouldInsert = !dropdown.inserted;
dropdown.inserted = true;
}
return dropdown;
}
//-----------------------------------------------------------------------------
@@ -181,36 +219,16 @@ export class KanbanCompiler extends ViewCompiler {
* @returns {Element | null}
*/
compileDropdown(el, params) {
const dropdownEl = this.renderDropdown();
const { shouldInsert, el: dropdownEl } = this.renderDropdown("dropdown");
const classes = [...el.classList].filter((cls) => cls && cls !== "dropdown").join(" ");
combineAttributes(this.dropdown, "class", toStringExpression(classes), "+' '+");
combineAttributes(dropdownEl, "class", toStringExpression(classes), "+' '+");
for (const child of el.childNodes) {
append(this.dropdown, this.compileNode(child, params));
append(dropdownEl, this.compileNode(child, params));
}
return dropdownEl;
}
/**
* @param {Element} el
* @param {Object} params
* @returns {Element | null}
*/
compileDropdownButton(el, params) {
const dropdownEl = this.renderDropdown();
const classes = ["btn", ...el.classList].filter(Boolean).join(" ");
const togglerSlot = createElement("t", { "t-set-slot": "toggler" });
combineAttributes(this.dropdown, "togglerClass", toStringExpression(classes), "+' '+");
for (const child of el.childNodes) {
append(togglerSlot, this.compileNode(child, params));
}
append(this.dropdown, togglerSlot);
return dropdownEl;
return shouldInsert && dropdownEl;
}
/**
@@ -219,16 +237,36 @@ export class KanbanCompiler extends ViewCompiler {
* @returns {Element | null}
*/
compileDropdownMenu(el, params) {
const dropdownEl = this.renderDropdown();
const { shouldInsert, el: dropdownEl } = this.renderDropdown("menu");
const cls = el.getAttribute("class") || "";
combineAttributes(this.dropdown, "menuClass", toStringExpression(cls), "+' '+");
combineAttributes(dropdownEl, "menuClass", toStringExpression(cls), "+' '+");
for (const child of el.childNodes) {
append(this.dropdown, this.compileNode(child, params));
append(dropdownEl, this.compileNode(child, params));
}
return dropdownEl;
return shouldInsert && dropdownEl;
}
/**
* @param {Element} el
* @param {Object} params
* @returns {Element | null}
*/
compileDropdownToggler(el, params) {
const { shouldInsert, el: dropdownEl } = this.renderDropdown("toggler");
const classes = ["btn", ...el.classList].filter(Boolean).join(" ");
const togglerSlot = createElement("t", { "t-set-slot": "toggler" });
combineAttributes(dropdownEl, "togglerClass", toStringExpression(classes), "+' '+");
for (const child of el.childNodes) {
append(togglerSlot, this.compileNode(child, params));
}
append(dropdownEl, togglerSlot);
return shouldInsert && dropdownEl;
}
/**
@@ -9881,20 +9881,21 @@ QUnit.module("Views", (hooks) => {
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
<ul class="o_kanban_card_manage_pane dropdown-menu" role="menu">
<li>Hello</li>
</ul>
</div>
</t>
</templates>
</kanban>
`,
arch: /* xml */ `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
<div class="dropdown" />
<ul class="o_kanban_card_manage_pane dropdown-menu" role="menu">
<li>Hello</li>
</ul>
</div>
</t>
</templates>
</kanban>
`,
});
const dropdown = target.querySelector(".o_kanban_record .o-dropdown");
@@ -9912,20 +9913,20 @@ QUnit.module("Views", (hooks) => {
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
<a class="o_kanban_manage_toggle_button o_left" href="#">
<i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/>
</a>
</div>
</t>
</templates>
</kanban>
`,
arch: /* xml */ `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
<a class="o_kanban_manage_toggle_button o_left" href="#">
<i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/>
</a>
</div>
</t>
</templates>
</kanban>
`,
});
const dropdown = target.querySelector(".o_kanban_record .o-dropdown");
@@ -9944,7 +9945,7 @@ QUnit.module("Views", (hooks) => {
type: "kanban",
resModel: "partner",
serverData,
arch: `
arch: /* xml */ `
<kanban>
<templates>
<t t-name="kanban-box">
@@ -9968,6 +9969,79 @@ QUnit.module("Views", (hooks) => {
}
);
QUnit.test("declaring only the menu does not insert a dropdown", async (assert) => {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: /* xml */ `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
<ul class="o_kanban_card_manage_pane dropdown-menu" role="menu">
<li>Hello</li>
</ul>
</div>
</t>
</templates>
</kanban>
`,
});
assert.containsNone(target, ".o_kanban_record .o-dropdown");
});
QUnit.test("support multiple dropdowns", async (assert) => {
assert.expect(4);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: /* xml */ `
<kanban>
<field name="bar" />
<templates>
<div t-name="kanban-box">
<field name="foo" />
<t t-if="record.bar.raw_value">
<div class="dropdown dropdown-true">
<div class="dropdown-toggle">
<span class="toggler-true">TOGGLER TRUE</span>
</div>
<div class="dropdown-menu">
<span class="menu-true">MENU TRUE</span>
</div>
</div>
</t>
<t t-else="">
<div class="dropdown-toggle">
<span class="toggler-false">TOGGLER FALSE</span>
</div>
<div class="dropdown-menu">
<span class="menu-false">MENU FALSE</span>
</div>
</t>
</div>
</templates>
</kanban>
`,
});
assert.containsN(target, ".o_kanban_renderer .dropdown-true", 3);
assert.containsN(target, ".o_kanban_renderer .dropdown", 4);
await toggleRecordDropdown(2);
assert.strictEqual(getCardTexts()[2], "gnap\nTOGGLER TRUE\nMENU TRUE");
await toggleRecordDropdown(3);
assert.strictEqual(getCardTexts()[3], "blip\nTOGGLER FALSE\nMENU FALSE");
});
QUnit.test("can use JSON in kanban template", async (assert) => {
serverData.models.partner.records = [{ id: 1, foo: '["g", "e", "d"]' }];
await makeView({