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