From a1f286916de099560e0f344def3b2d7ffa9fec38 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Fri, 13 Aug 2021 15:40:14 +0000 Subject: [PATCH] [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) --- addons/web/__manifest__.py | 3 +- .../src/search/control_panel/control_panel.js | 53 +++++++++ .../control_panel}/control_panel.scss | 1 - .../search/control_panel/control_panel.xml | 60 ++++++++++ .../src/webclient/actions/action_service.js | 9 +- .../web/static/tests/search/control_panel.js | 108 ++++++++++++++++++ 6 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 addons/web/static/src/search/control_panel/control_panel.js rename addons/web/static/src/{legacy/scss => search/control_panel}/control_panel.scss (98%) create mode 100644 addons/web/static/src/search/control_panel/control_panel.xml create mode 100644 addons/web/static/tests/search/control_panel.js diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index e13682633a6..63b0a7d7f47 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -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'), diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js new file mode 100644 index 00000000000..7a15102ed59 --- /dev/null +++ b/addons/web/static/src/search/control_panel/control_panel.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: {}, +}; diff --git a/addons/web/static/src/legacy/scss/control_panel.scss b/addons/web/static/src/search/control_panel/control_panel.scss similarity index 98% rename from addons/web/static/src/legacy/scss/control_panel.scss rename to addons/web/static/src/search/control_panel/control_panel.scss index fd2516478f7..5a121bdc2d5 100644 --- a/addons/web/static/src/legacy/scss/control_panel.scss +++ b/addons/web/static/src/search/control_panel/control_panel.scss @@ -31,7 +31,6 @@ .o_cp_bottom_left { display: flex; - justify-content: space-between; > .o_cp_action_menus { padding-right: 10px; diff --git a/addons/web/static/src/search/control_panel/control_panel.xml b/addons/web/static/src/search/control_panel/control_panel.xml new file mode 100644 index 00000000000..fb3a87ea024 --- /dev/null +++ b/addons/web/static/src/search/control_panel/control_panel.xml @@ -0,0 +1,60 @@ + + + + +
+
+
+ + + +
+
+ +
+
+
+
+ +
+
+ + +
+ + + + +
+
+
+ + + + + + + diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index c711928e28e..7a063912cd2 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -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), { diff --git a/addons/web/static/tests/search/control_panel.js b/addons/web/static/tests/search/control_panel.js new file mode 100644 index 00000000000..6285de74c6d --- /dev/null +++ b/addons/web/static/tests/search/control_panel.js @@ -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"]); +});