From 2b7f49744201b847a87101ea7904ccdf727f6fe8 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Wed, 29 Jun 2022 10:12:44 +0000 Subject: [PATCH] [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 --- .../src/views/kanban/kanban_compiler.js | 130 +++++++++++------ .../static/tests/views/kanban_view_tests.js | 132 ++++++++++++++---- 2 files changed, 187 insertions(+), 75 deletions(-) diff --git a/addons/web/static/src/views/kanban/kanban_compiler.js b/addons/web/static/src/views/kanban/kanban_compiler.js index b1c431da086..1baaed77ac0 100644 --- a/addons/web/static/src/views/kanban/kanban_compiler.js +++ b/addons/web/static/src/views/kanban/kanban_compiler.js @@ -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} */ + 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; } /** diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index 032285fcd46..e875685517f 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -9881,20 +9881,21 @@ QUnit.module("Views", (hooks) => { type: "kanban", resModel: "partner", serverData, - arch: ` - - - -
- - -
-
-
-
- `, + arch: /* xml */ ` + + + +
+ + + + + + `, }); const dropdown = target.querySelector(".o_kanban_record .o-dropdown"); @@ -9912,20 +9913,20 @@ QUnit.module("Views", (hooks) => { type: "kanban", resModel: "partner", serverData, - arch: ` - - - -
- - - - -
-
-
-
- `, + arch: /* xml */ ` + + + +
+ + + + +
+
+
+
+ `, }); 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 */ ` @@ -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 */ ` + + + +
+ + +
+
+
+
+ `, + }); + + 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 */ ` + + + +
+ + + + + + + + +
+
+
+ `, + }); + + 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({