[IMP] web: new ControlPanel class

We introduce here an incomplete implementation of the new ControlPanel.
The idea is to allow the rewritting of the client actions that do not
use the search view but only the breadcrumbs or the button zone...
The full implementation of the new ControlPanel will be merged when the
new search infrastructure is ready to be merged.

closes odoo/odoo#75083

Related: odoo/enterprise#20231
Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2021-08-16 16:03:30 +00:00
parent 00e3cb74c4
commit a1f286916d
6 changed files with 229 additions and 5 deletions
+2 -1
View File
@@ -92,6 +92,7 @@ This module provides the core of the Odoo Web Client.
'web/static/src/core/utils/transitions.scss',
'web/static/src/core/**/*',
'web/static/src/fields/**/*',
'web/static/src/search/**/*',
'web/static/src/views/**/*',
'web/static/src/webclient/**/*',
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
@@ -110,7 +111,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/scss/tooltip.scss',
'web/static/src/legacy/scss/switch_company_menu.scss',
'web/static/src/legacy/scss/ace.scss',
'web/static/src/legacy/scss/control_panel.scss',
'web/static/src/legacy/scss/fields.scss',
'web/static/src/legacy/scss/file_upload.scss',
'web/static/src/legacy/scss/views.scss',
@@ -480,6 +480,7 @@ This module provides the core of the Odoo Web Client.
'web/static/tests/env_tests.js',
'web/static/tests/core/**/*.js',
'web/static/tests/fields/**/*.js',
'web/static/tests/search/**/*.js',
'web/static/tests/views/**/*.js',
'web/static/tests/webclient/**/*.js',
('remove', 'web/static/tests/webclient/**/helpers.js'),
@@ -0,0 +1,53 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
const { Component } = owl;
export class ControlPanel extends Component {
setup() {
this.actionService = useService("action");
}
/**
* @returns {Object}
*/
get display() {
const display = Object.assign(
{
"top-left": true,
"top-right": true,
"bottom-left": true,
"bottom-right": true,
},
this.props.display
);
display.top = display["top-left"] || display["top-right"];
display.bottom = display["bottom-left"] || display["bottom-right"];
return display;
}
/**
* Called when an element of the breadcrumbs is clicked.
*
* @param {string} jsId
*/
onBreadcrumbClicked(jsId) {
this.actionService.restore(jsId);
}
/**
* Called when a view is clicked in the view switcher.
*
* @param {ViewType} viewType
*/
onViewClicked(viewType) {
this.actionService.switchView(viewType);
}
}
ControlPanel.template = "web.ControlPanel";
ControlPanel.defaultProps = {
breadcrumbs: [],
display: {},
};
@@ -31,7 +31,6 @@
.o_cp_bottom_left {
display: flex;
justify-content: space-between;
> .o_cp_action_menus {
padding-right: 10px;
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.ControlPanel" owl="1">
<div class="o_control_panel">
<div t-if="display['top']" class="o_cp_top">
<div t-if="display['top-left']" class="o_cp_top_left">
<t t-slot="control-panel-top-left">
<t t-call="web.Breadcrumbs" />
</t>
</div>
<div t-if="display['top-right']" class="o_cp_top_right">
<t t-slot="control-panel-top-right"/>
</div>
</div>
<div t-if="display['bottom']" class="o_cp_bottom">
<div t-if="display['bottom-left']" class="o_cp_bottom_left">
<t t-slot="control-panel-bottom-left"/>
</div>
<div t-if="display['bottom-right']" class="o_cp_bottom_right">
<t t-slot="control-panel-bottom-right"/>
<div class="o_cp_pager"/>
<t t-if="props.viewSwitcherEntries and props.viewSwitcherEntries.length > 1">
<nav class="btn-group o_cp_switch_buttons">
<t t-foreach="props.viewSwitcherEntries" t-as="view" t-key="view.type">
<button class="btn btn-secondary fa fa-lg o_switch_view "
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
t-att-data-tooltip="view.name"
t-on-click="onViewClicked(view.type)"
/>
</t>
</nav>
</t>
</div>
</div>
</div>
</t>
<t t-name="web.Breadcrumbs" owl="1">
<ol class="breadcrumb">
<t t-foreach="props.breadcrumbs or []" t-as="breadcrumb" t-key="breadcrumb.jsId">
<li class="breadcrumb-item"
t-att-class="{ o_back_button: breadcrumb_last}"
t-on-click.prevent="onBreadcrumbClicked(breadcrumb.jsId)"
>
<a href="#">
<t t-esc="breadcrumb.name"/>
</a>
</li>
</t>
<li class="breadcrumb-item active">
<t t-if="props.displayName" t-esc="props.displayName"/>
<em t-else="" class="text-warning">Unnamed</em>
</li>
</ol>
</t>
</templates>
@@ -370,13 +370,16 @@ function makeActionManager(env) {
const viewSwitcherEntries = views
.filter((v) => v.multiRecord === view.multiRecord)
.map((v) => {
return {
// FIXME: missing accesskey
const viewSwitcherEntry = {
icon: v.icon,
name: v.display_name,
name: v.display_name.toString(),
type: v.type,
multiRecord: v.multiRecord,
};
if (view.type === v.type) {
viewSwitcherEntry.active = true;
}
return viewSwitcherEntry;
});
const flags = action.flags || {};
const viewProps = Object.assign(_getActionProps(action, props), {
@@ -0,0 +1,108 @@
/** @odoo-module **/
import { actionService } from "@web/webclient/actions/action_service";
import { click } from "@web/../tests/helpers/utils";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { getFixture } from "@web/../tests/helpers/utils";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { notificationService } from "@web/core/notifications/notification_service";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { registry } from "@web/core/registry";
const serviceRegistry = registry.category("services");
const { mount } = owl;
/**
* @param {MakeViewParams} params
* @returns {owl.Component}
*/
export async function makeControlPanel(params = {}) {
const serverData = params.serverData;
const mockRPC = params.mockRPC;
const props = Object.assign({}, params);
delete props.serverData;
delete props.mockRPC;
const env = await makeTestEnv({ serverData, mockRPC });
const target = getFixture();
const controlPanel = await mount(ControlPanel, { env, props, target });
registerCleanup(() => controlPanel.destroy());
return controlPanel;
}
QUnit.module("ControlPanel", {
async beforeEach() {
serviceRegistry.add("action", actionService);
serviceRegistry.add("notification", notificationService);
},
});
QUnit.test("simple rendering", async (assert) => {
const controlPanel = await makeControlPanel({
display: {
"top-right": false,
},
});
assert.containsOnce(controlPanel, ".o_cp_top");
assert.containsOnce(controlPanel, ".o_cp_top_left");
assert.containsNone(controlPanel, ".o_cp_top_right");
assert.containsOnce(controlPanel, ".o_cp_bottom");
assert.containsOnce(controlPanel, ".o_cp_bottom_left");
assert.containsOnce(controlPanel, ".o_cp_bottom_right");
assert.containsNone(controlPanel, ".o_cp_switch_buttons");
assert.containsOnce(controlPanel, ".breadcrumb");
assert.containsOnce(controlPanel, ".breadcrumb li.breadcrumb-item");
assert.strictEqual(controlPanel.el.querySelector("li.breadcrumb-item").innerText, "Unnamed");
});
QUnit.test("breadcrumbs prop", async (assert) => {
const controlPanel = await makeControlPanel({
breadcrumbs: [{ jsId: "controller_7", name: "Previous" }],
displayName: "Current",
});
assert.containsN(controlPanel, ".breadcrumb li.breadcrumb-item", 2);
const breadcrumbItems = controlPanel.el.querySelectorAll("li.breadcrumb-item");
assert.strictEqual(breadcrumbItems[0].innerText, "Previous");
assert.hasClass(breadcrumbItems[1], "active");
assert.strictEqual(breadcrumbItems[1].innerText, "Current");
controlPanel.env.services.action.restore = (jsId) => {
assert.step(jsId);
};
await click(breadcrumbItems[0]);
assert.verifySteps(["controller_7"]);
});
QUnit.test("viewSwitcherEntries prop", async (assert) => {
const controlPanel = await makeControlPanel({
viewSwitcherEntries: [
{ type: "list", active: true, icon: "fa-list-ul", name: "List" },
{ type: "kanban", icon: "fa-th-large", name: "Kanban" },
],
});
assert.containsOnce(controlPanel, ".o_cp_switch_buttons");
assert.containsN(controlPanel, ".o_switch_view", 2);
const views = controlPanel.el.querySelectorAll(".o_switch_view");
assert.strictEqual(views[0].getAttribute("data-tooltip"), "List");
assert.hasClass(views[0], "active");
assert.strictEqual(views[1].getAttribute("data-tooltip"), "Kanban");
assert.hasClass(views[1], "fa-th-large");
controlPanel.env.services.action.switchView = (viewType) => {
assert.step(viewType);
};
await click(views[1]);
assert.verifySteps(["kanban"]);
});