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"]); +});