[IMP] web: add buttons in control panel in kanban

This commit adds the possibility to define buttons that are always
present in the arch of a kanban view.

Purpose:
Several customisations of the kanban view consist in adding an always
present button in the control panel that performs an action/calls a
model's method.
We want to support this feature directly in the framework to remove
these customisations.
The task 3082303 has added the same functionality to the list view.

Solution:
Add a "display" option that lets you know when to display a button.
The different values that can be assigned to "display":
- "selection": the button is only displayed if at least one record is selected.
- "always": the button is always displayed.
The default value of "display" is "selection".
Currently, only the value "always" is implemented because it is not
possible to select a record in a kanban. The selection of a record in a
kanban view will be implemented soon, so we decided to use the same API
as in the list view.

Exemple:
<kanban>
    <header>
        <button name="to_do" type="object" string="Do" display="always"/>
    </header>
    ...
</kaban>

Taskid: 3082390
Part-of: odoo/odoo#106732
This commit is contained in:
FrancoisGe
2023-01-16 10:32:05 +01:00
parent dab3becf38
commit 469577ac2f
7 changed files with 127 additions and 30 deletions
@@ -1,13 +1,14 @@
/** @odoo-module **/
import { extractAttributes, XMLParser } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
import {
addFieldDependencies,
archParseBoolean,
getActiveActions,
processButton,
stringToOrderBy,
} from "@web/views/utils";
import { extractAttributes, XMLParser } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
import { Widget } from "@web/views/widgets/widget";
/**
@@ -57,12 +58,25 @@ export class KanbanArchParser extends XMLParser {
const openAction = action && type ? { action, type } : null;
const templateDocs = {};
const activeFields = {};
let headerButtons = [];
let buttonId = 0;
// Root level of the template
this.visitXML(xmlDoc, (node) => {
if (node.hasAttribute("t-name")) {
templateDocs[node.getAttribute("t-name")] = node;
return;
}
if (node.tagName === "header") {
headerButtons = [...node.children]
.filter((node) => node.tagName === "button")
.map((node) => ({
...processButton(node),
type: "button",
id: buttonId++,
}))
.filter((button) => button.modifiers.invisible !== true);
return false;
}
// Case: field node
if (node.tagName === "field") {
// In kanban, we display many2many fields as tags by default
@@ -145,6 +159,7 @@ export class KanbanArchParser extends XMLParser {
defaultGroupBy,
fieldNodes,
handleField,
headerButtons,
colorField,
defaultOrder,
onCreate,
@@ -5,8 +5,9 @@ import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { useModel } from "@web/views/model";
import { standardViewProps } from "@web/views/standard_view_props";
import { useSetupView } from "@web/views/view_hook";
import { MultiRecordViewButton } from "@web/views/view_button/multi_record_view_button";
import { useViewButtons } from "@web/views/view_button/view_button_hook";
import { useSetupView } from "@web/views/view_hook";
import { KanbanRenderer } from "./kanban_renderer";
import { Component, useRef } from "@odoo/owl";
@@ -34,6 +35,7 @@ export class KanbanController extends Component {
tooltipInfo: archInfo.tooltipInfo,
rootState,
});
this.headerButtons = archInfo.headerButtons;
this.rootRef = useRef("root");
useViewButtons(this.model, this.rootRef, {
@@ -128,7 +130,7 @@ export class KanbanController extends Component {
}
KanbanController.template = `web.KanbanView`;
KanbanController.components = { Layout, KanbanRenderer };
KanbanController.components = { Layout, KanbanRenderer, MultiRecordViewButton };
KanbanController.props = {
...standardViewProps,
defaultGroupBy: { validate: (dgb) => !dgb || typeof dgb === "string", optional: true },
@@ -29,6 +29,19 @@
New
</button>
</t>
<t t-foreach="headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="button.display === 'always'"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
</div>
</t>
@@ -12,6 +12,7 @@ import { session } from "@web/session";
import { useModel } from "@web/views/model";
import { DynamicRecordList } from "@web/views/relational_model";
import { standardViewProps } from "@web/views/standard_view_props";
import { MultiRecordViewButton } from "@web/views/view_button/multi_record_view_button";
import { ViewButton } from "@web/views/view_button/view_button";
import { useViewButtons } from "@web/views/view_button/view_button_hook";
import { ExportDataDialog } from "@web/views/view_dialogs/export_data_dialog";
@@ -19,30 +20,6 @@ import { useSetupView } from "@web/views/view_hook";
import { Component, onMounted, onWillStart, useEffect, useRef, useSubEnv } from "@odoo/owl";
export class ListViewHeaderButton extends ViewButton {
async onClick() {
const { clickParams, list } = this.props;
const resIds = await list.getResIds(true);
clickParams.buttonContext = {
active_domain: this.props.domain,
// active_id: resIds[0], // FGE TODO
active_ids: resIds,
active_model: list.resModel,
};
this.env.onClickViewButton({
clickParams,
getResParams: () => ({
context: list.context,
evalContext: list.evalContext,
resModel: list.resModel,
resIds,
}),
});
}
}
ListViewHeaderButton.props = [...ViewButton.props, "list", "domain"];
// -----------------------------------------------------------------------------
export class ListController extends Component {
@@ -518,7 +495,7 @@ export class ListController extends Component {
}
ListController.template = `web.ListView`;
ListController.components = { ActionMenus, ListViewHeaderButton, Layout, ViewButton };
ListController.components = { ActionMenus, Layout, ViewButton, MultiRecordViewButton };
ListController.props = {
...standardViewProps,
allowSelectors: { type: Boolean, optional: true },
@@ -48,7 +48,7 @@
</button>
</t>
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<ListViewHeaderButton
<MultiRecordViewButton
t-if="nbSelected || (button.display === 'always')"
list="model.root"
className="button.className"
@@ -0,0 +1,27 @@
/** @odoo-module */
import { ViewButton } from "./view_button";
export class MultiRecordViewButton extends ViewButton {
async onClick() {
const { clickParams, list } = this.props;
const resIds = await list.getResIds(true);
clickParams.buttonContext = {
active_domain: this.props.domain,
active_ids: resIds,
active_model: list.resModel,
};
this.env.onClickViewButton({
clickParams,
getResParams: () => ({
context: list.context,
evalContext: list.evalContext,
resModel: list.resModel,
resIds,
}),
});
}
}
MultiRecordViewButton.props = [...ViewButton.props, "list", "domain"];
@@ -17,6 +17,7 @@ import {
triggerHotkey,
} from "@web/../tests/helpers/utils";
import {
getButtons,
getFacetTexts,
getPagerLimit,
getPagerValue,
@@ -12356,4 +12357,66 @@ QUnit.module("Views", (hooks) => {
await click(column1);
assert.verifySteps(["scrolled"]);
});
QUnit.test(
"kanban view: action button in controlPanel with display='always'",
async (assert) => {
const domain = [["id", "=", 1]];
const kanban = await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban class="o_kanban_test">
<header>
<button name="display" type="object" class="display" string="display" display="always"/>
<button name="default-selection" type="object" class="default-selection" string="default-selection"/>
</header>
<field name="bar" />
<templates>
<t t-name="kanban-box">
<div>
<field name="foo" />
</div>
</t>
</templates>
</kanban>`,
domain,
});
patchWithCleanup(kanban.env.services.action, {
doActionButton: async (params) => {
const { buttonContext, context, name, resModel, resIds, type } = params;
assert.step("execute_action");
// Action's own properties
assert.strictEqual(name, "display");
assert.strictEqual(type, "object");
// The action's execution context
assert.deepEqual(buttonContext, {
active_domain: domain,
active_ids: [],
active_model: "partner",
});
assert.deepEqual(context, {
lang: "en",
tz: "taht",
uid: 7,
});
assert.strictEqual(resModel, "partner");
assert.deepEqual([...resIds], []);
},
});
const cpButtons = getButtons(target);
assert.deepEqual(
[...cpButtons].map((button) => button.textContent.trim()),
["New", "display"]
);
assert.hasClass(cpButtons[1], "display");
await click(cpButtons[1]);
assert.verifySteps(["execute_action"]);
}
);
});