[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:
committed by
Aaron Bohy
parent
6cdd91b614
commit
2b7f497442
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user