diff --git a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js index 2a61917394f..9944c0d0ecb 100644 --- a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js +++ b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js @@ -20,6 +20,11 @@ }, { trigger: '.o_control_panel .o_cp_action_menus .dropdown-toggle', extra_trigger: '.o_breadcrumb .active:contains("INV/")', + }, { + trigger: `.o_control_panel .o_cp_action_menus .dropdown-toggle:contains("${_t("Print")}")`, + run: function () { + this.$anchor[0].dispatchEvent(new MouseEvent("mouseenter")); + }, }, { trigger: '.o_control_panel .o_cp_action_menus .o_menu_item:contains("' + _t('Invoices without Payment') + '")', }, { diff --git a/addons/base_import/static/src/import_records/import_records.js b/addons/base_import/static/src/import_records/import_records.js index 83e7ce22bec..7f947db0d87 100644 --- a/addons/base_import/static/src/import_records/import_records.js +++ b/addons/base_import/static/src/import_records/import_records.js @@ -4,6 +4,7 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Component } from "@odoo/owl"; +import { STATIC_ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus"; const cogMenuRegistry = registry.category("cogMenu"); @@ -37,7 +38,7 @@ export class ImportRecords extends Component { export const importRecordsItem = { Component: ImportRecords, - groupNumber: 4, + groupNumber: STATIC_ACTIONS_GROUP_NUMBER, isDisplayed: ({ config, isSmall }) => !isSmall && config.actionType === "ir.actions.act_window" && diff --git a/addons/base_import/static/src/import_records/import_records.xml b/addons/base_import/static/src/import_records/import_records.xml index d8aa7c6a9b2..ca09b33448d 100644 --- a/addons/base_import/static/src/import_records/import_records.xml +++ b/addons/base_import/static/src/import_records/import_records.xml @@ -3,7 +3,7 @@ - Import records + Import records diff --git a/addons/base_import/static/tests/import_records_tests.js b/addons/base_import/static/tests/import_records_tests.js index 7a578157f82..b0ce7cc9225 100644 --- a/addons/base_import/static/tests/import_records_tests.js +++ b/addons/base_import/static/tests/import_records_tests.js @@ -76,8 +76,7 @@ QUnit.module("Base Import Tests", (hooks) => { }, }); - await toggleActionMenu(target); - assert.containsOnce(target, ".o_cp_action_menus .o-dropdown--menu"); + assert.containsNone(target, ".o_cp_action_menus"); assert.containsNone(target, ".o_import_menu"); } ); @@ -95,8 +94,7 @@ QUnit.module("Base Import Tests", (hooks) => { }, }); - await toggleActionMenu(target); - assert.containsOnce(target, ".o_cp_action_menus .o-dropdown--menu"); + assert.containsNone(target, ".o_cp_action_menus"); assert.containsNone(target, ".o_import_menu"); } ); @@ -239,8 +237,7 @@ QUnit.module("Base Import Tests", (hooks) => { await selectDropdownItem(target, "m2o", "Search More..."); const dialog = target.querySelector(".modal"); - await toggleActionMenu(dialog); - assert.containsOnce(dialog, ".o_cp_action_menus .o-dropdown--menu"); + assert.containsNone(dialog, ".o_cp_action_menus"); assert.containsNone(dialog, ".o_import_menu"); } ); diff --git a/addons/board/static/src/add_to_board/add_to_board.js b/addons/board/static/src/add_to_board/add_to_board.js index 7b257200e60..2f3b3e48cb4 100644 --- a/addons/board/static/src/add_to_board/add_to_board.js +++ b/addons/board/static/src/add_to_board/add_to_board.js @@ -95,7 +95,7 @@ AddToBoard.components = { Dropdown }; export const addToBoardItem = { Component: AddToBoard, - groupNumber: 4, + groupNumber: 20, isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window" && config.actionId, }; diff --git a/addons/board/static/src/add_to_board/add_to_board.xml b/addons/board/static/src/add_to_board/add_to_board.xml index 3f7ebe33e15..6be70d40fa1 100644 --- a/addons/board/static/src/add_to_board/add_to_board.xml +++ b/addons/board/static/src/add_to_board/add_to_board.xml @@ -3,11 +3,13 @@ - Add to my dashboard -
- -
-
+ + Dashboard Icon + Dashboard + +
+ +
diff --git a/addons/web/static/src/search/action_menus/action_menus.js b/addons/web/static/src/search/action_menus/action_menus.js index 00027d843df..300644fb0e5 100644 --- a/addons/web/static/src/search/action_menus/action_menus.js +++ b/addons/web/static/src/search/action_menus/action_menus.js @@ -8,6 +8,10 @@ import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { useService } from "@web/core/utils/hooks"; import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl"; + +export const STATIC_ACTIONS_GROUP_NUMBER = 1; +export const ACTIONS_GROUP_NUMBER = 100; + /** * Action menus (or Action/Print bar, previously called 'Sidebar') * @@ -46,12 +50,16 @@ export class ActionMenus extends Component { async getActionItems(props) { return (props.items.action || []).map((action) => { if (action.callback) { - return Object.assign({ key: `action-${action.description}` }, action); + return Object.assign( + { key: `action-${action.description}`, groupNumber: ACTIONS_GROUP_NUMBER }, + action + ); } else { return { action, description: action.name, key: action.id, + groupNumber: action.groupNumber || ACTIONS_GROUP_NUMBER, }; } }); diff --git a/addons/web/static/src/search/action_menus/action_menus.xml b/addons/web/static/src/search/action_menus/action_menus.xml index 141c0ea4a1c..e7523b3cc44 100644 --- a/addons/web/static/src/search/action_menus/action_menus.xml +++ b/addons/web/static/src/search/action_menus/action_menus.xml @@ -18,11 +18,12 @@ - Action + Actions + diff --git a/addons/web/static/src/search/cog_menu/action_menus_items.js b/addons/web/static/src/search/cog_menu/action_menus_items.js deleted file mode 100644 index 1dc0406c531..00000000000 --- a/addons/web/static/src/search/cog_menu/action_menus_items.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @odoo-module **/ - -import { ActionMenus } from "@web/search/action_menus/action_menus"; - - -/** - * Combined Action menus (or Action/Print bar, previously called 'Sidebar') - * - * This is a variation of the ActionMenus, combined into a single DropDown. - * - * The side bar is the group of dropdown menus located on the left side of the - * control panel. Its role is to display a list of items depending on the view - * type and selected records and to execute a set of actions on active records. - * It is made out of 2 dropdown: Print and Action. - * - * @extends Component - */ -export class ActionMenusItems extends ActionMenus { - static template = "web.ActionMenusItems"; -} diff --git a/addons/web/static/src/search/cog_menu/action_menus_items.xml b/addons/web/static/src/search/cog_menu/action_menus_items.xml deleted file mode 100644 index b38bada3f18..00000000000 --- a/addons/web/static/src/search/cog_menu/action_menus_items.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/web/static/src/search/cog_menu/cog_menu.js b/addons/web/static/src/search/cog_menu/cog_menu.js index 282604bb688..5e86fe0fe42 100644 --- a/addons/web/static/src/search/cog_menu/cog_menu.js +++ b/addons/web/static/src/search/cog_menu/cog_menu.js @@ -2,8 +2,9 @@ import { registry } from "@web/core/registry"; import { Dropdown } from "@web/core/dropdown/dropdown"; +import { ActionMenus } from "@web/search/action_menus/action_menus"; -import { Component } from "@odoo/owl"; +import { onWillStart, onWillUpdateProps } from "@odoo/owl"; const cogMenuRegistry = registry.category("cogMenu"); @@ -17,32 +18,61 @@ const cogMenuRegistry = registry.category("cogMenu"); * type and selected records and to execute a set of actions on active records. * It is made out of 2 dropdown: Print and Action. * - * @extends Component + * @extends ActionMenus */ -export class CogMenu extends Component { +export class CogMenu extends ActionMenus { static template = "web.CogMenu"; static components = { + ...ActionMenus.components, Dropdown, }; static props = { - slots: { type: Object, optional: true }, + ...ActionMenus.props, + getActiveIds: { type: ActionMenus.props.getActiveIds, optional: true }, + context: { type: ActionMenus.props.context, optional: true }, + resModel: { type: ActionMenus.props.resModel, optional: true }, + items: { ...ActionMenus.props.items, optional: true }, + }; + static defaultProps = { + ...ActionMenus.defaultProps, + items: {}, }; - get hasItems() { - return this.cogItems.length || !!this.props.slots?.default; + setup() { + super.setup(); + onWillStart(async () => { + this.registryItems = await this._registryItems(); + }); + onWillUpdateProps(async () => { + this.registryItems = await this._registryItems(); + }); } - get cogItems() { - const registryMenus = []; + get hasItems() { + return this.cogItems.length || this.printItems.length; + } + + async _registryItems() { + const items = []; for (const item of cogMenuRegistry.getAll()) { - if ("isDisplayed" in item ? item.isDisplayed(this.env) : true) { - registryMenus.push({ + if ("isDisplayed" in item ? await item.isDisplayed(this.env) : true) { + items.push({ Component: item.Component, groupNumber: item.groupNumber, key: item.Component.name, }); } } - return registryMenus; + return items; + } + + get cogItems() { + return [...this.actionItems, ...this.registryItems].sort((item1, item2) => { + const grp = (item1.groupNumber || 0) - (item2.groupNumber || 0); + if (grp !== 0) { + return grp; + } + return (item1.sequence || 0) - (item2.sequence || 0); + }); } } diff --git a/addons/web/static/src/search/cog_menu/cog_menu.scss b/addons/web/static/src/search/cog_menu/cog_menu.scss new file mode 100644 index 00000000000..6da63803721 --- /dev/null +++ b/addons/web/static/src/search/cog_menu/cog_menu.scss @@ -0,0 +1,12 @@ +.o_cp_action_menus { + .o_cp_action_app_icon { + width: var(--oi-font-size, 1em); + vertical-align: -0.2em; + } + + .dropdown-item:not(.focus) > .o_cp_action_app_icon { + // Temporary workaround until we'll support inline SVGs icons + // globally (eg. Spreadsheet's toolbar icons) + filter: saturate(0%) brightness(0%) invert(23%) sepia(7%) saturate(1896%) hue-rotate(179deg) brightness(91%) contrast(88%); + } +} diff --git a/addons/web/static/src/search/cog_menu/cog_menu.xml b/addons/web/static/src/search/cog_menu/cog_menu.xml index 6fa1fff0a94..e175fe391ec 100644 --- a/addons/web/static/src/search/cog_menu/cog_menu.xml +++ b/addons/web/static/src/search/cog_menu/cog_menu.xml @@ -7,14 +7,42 @@ - + + + + + Print + + + + + + + + + + + + diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index f493455342d..20029c64b78 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -21,7 +21,7 @@ import { ButtonBox } from "./button_box/button_box"; import { ViewButton } from "@web/views/view_button/view_button"; import { Field } from "@web/views/fields/field"; import { CogMenu } from "@web/search/cog_menu/cog_menu"; -import { ActionMenusItems } from "@web/search/cog_menu/action_menus_items"; +import { STATIC_ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus"; import { Component, onRendered, useEffect, useRef } from "@odoo/owl"; import { useViewCompiler } from "../view_compiler"; @@ -327,6 +327,7 @@ export class FormController extends Component { isAvailable: () => this.archiveEnabled && this.model.root.isActive, sequence: 10, description: this.env._t("Archive"), + icon: "oi oi-archive", callback: () => { const dialogProps = { body: this.env._t("Are you sure that you want to archive this record?"), @@ -340,18 +341,21 @@ export class FormController extends Component { unarchive: { isAvailable: () => this.archiveEnabled && !this.model.root.isActive, sequence: 20, + icon: "oi oi-unarchive", description: this.env._t("Unarchive"), callback: () => this.model.root.unarchive(), }, duplicate: { isAvailable: () => activeActions.create && activeActions.duplicate, sequence: 30, + icon: "fa fa-clone", description: this.env._t("Duplicate"), callback: () => this.duplicateRecord(), }, delete: { isAvailable: () => activeActions.delete && !this.model.root.isNew, sequence: 40, + icon: "fa fa-trash-o", description: this.env._t("Delete"), callback: () => this.deleteRecord(), skipSave: true, @@ -364,7 +368,11 @@ export class FormController extends Component { const staticActionItems = Object.entries(this.getStaticActionMenuItems()) .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable()) .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0)) - .map(([key, item]) => Object.assign({ key }, omit(item, "isAvailable", "sequence"))); + .map(([key, item]) => + Object.assign({ key }, omit(item, "isAvailable", "sequence"), { + groupNumber: STATIC_ACTIONS_GROUP_NUMBER, + }) + ); return { action: [...staticActionItems, ...(actionMenus.action || [])], @@ -511,7 +519,6 @@ FormController.components = { ViewButton, Field, CogMenu, - ActionMenusItems, }; FormController.props = { ...standardViewProps, diff --git a/addons/web/static/src/views/form/form_controller.xml b/addons/web/static/src/views/form/form_controller.xml index eb11532e4c8..22a93003f4d 100644 --- a/addons/web/static/src/views/form/form_controller.xml +++ b/addons/web/static/src/views/form/form_controller.xml @@ -29,20 +29,16 @@ - - - - - + diff --git a/addons/web/static/src/views/list/export_all/export_all.js b/addons/web/static/src/views/list/export_all/export_all.js new file mode 100644 index 00000000000..940acaafd6e --- /dev/null +++ b/addons/web/static/src/views/list/export_all/export_all.js @@ -0,0 +1,41 @@ +/** @odoo-module **/ + +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { registry } from "@web/core/registry"; +import { archParseBoolean } from "@web/views/utils"; +import { STATIC_ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus"; + +import { Component } from "@odoo/owl"; + +const cogMenuRegistry = registry.category("cogMenu"); + +/** + * 'Export All' menu + * + * This component is used to export all the records for particular model. + * @extends Component + */ +export class ExportAll extends Component { + static template = "web.ExportAll"; + static components = { DropdownItem }; + + //--------------------------------------------------------------------- + // Protected + //--------------------------------------------------------------------- + + async onDirectExportData() { + this.env.searchModel.trigger('direct-export-data'); + } +} + +export const exportAllItem = { + Component: ExportAll, + groupNumber: STATIC_ACTIONS_GROUP_NUMBER, + isDisplayed: async (env) => + env.config.viewType === "list" && + !env.model.root.selection.length && + await env.model.user.hasGroup("base.group_allow_export") && + archParseBoolean(env.config.viewArch.getAttribute("export_xlsx"), true), +}; + +cogMenuRegistry.add("export-all-menu", exportAllItem, { sequence: 10 }); diff --git a/addons/web/static/src/views/list/export_all/export_all.xml b/addons/web/static/src/views/list/export_all/export_all.xml new file mode 100644 index 00000000000..ad05f26352d --- /dev/null +++ b/addons/web/static/src/views/list/export_all/export_all.xml @@ -0,0 +1,10 @@ + + + + + + Export All + + + + diff --git a/addons/web/static/src/views/list/list_controller.js b/addons/web/static/src/views/list/list_controller.js index e4a2a41d067..d69ef2dee26 100644 --- a/addons/web/static/src/views/list/list_controller.js +++ b/addons/web/static/src/views/list/list_controller.js @@ -4,10 +4,10 @@ import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_d import { download } from "@web/core/network/download"; import { evaluateExpr } from "@web/core/py_js/py"; import { unique } from "@web/core/utils/arrays"; -import { useService } from "@web/core/utils/hooks"; +import { useService, useBus } from "@web/core/utils/hooks"; import { omit } from "@web/core/utils/objects"; import { sprintf } from "@web/core/utils/strings"; -import { ActionMenus } from "@web/search/action_menus/action_menus"; +import { ActionMenus, STATIC_ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus"; import { Layout } from "@web/search/layout"; import { usePager } from "@web/search/pager_hook"; import { session } from "@web/session"; @@ -22,10 +22,7 @@ import { useSetupView } from "@web/views/view_hook"; import { ListConfirmationDialog } from "./list_confirmation_dialog"; import { SearchBar } from "@web/search/search_bar/search_bar"; import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler"; -import { Dropdown } from "@web/core/dropdown/dropdown"; -import { DropdownItem } from "@web/core/dropdown/dropdown_item"; import { CogMenu } from "@web/search/cog_menu/cog_menu"; -import { ActionMenusItems } from "@web/search/cog_menu/action_menus_items"; import { Component, @@ -158,6 +155,7 @@ export class ListController extends Component { onWillPatch(() => { this.firstLoad = false; }); + useBus(this.env.searchModel, 'direct-export-data', this.onDirectExportData.bind(this)); } get modelParams() { @@ -294,12 +292,14 @@ export class ListController extends Component { export: { isAvailable: () => this.isExportEnable, sequence: 10, + icon: "fa fa-upload", description: this.env._t("Export"), callback: () => this.onExportData(), }, archive: { isAvailable: () => this.archiveEnabled && !isM2MGrouped, sequence: 20, + icon: "oi oi-archive", description: this.env._t("Archive"), callback: () => { const dialogProps = { @@ -318,12 +318,14 @@ export class ListController extends Component { unarchive: { isAvailable: () => this.archiveEnabled && !isM2MGrouped, sequence: 30, + icon: "oi oi-unarchive", description: this.env._t("Unarchive"), callback: () => this.toggleArchiveState(false), }, delete: { isAvailable: () => this.activeActions.delete && !isM2MGrouped, sequence: 40, + icon: "fa fa-trash-o", description: this.env._t("Delete"), callback: () => this.onDeleteSelectedRecords(), }, @@ -335,7 +337,12 @@ export class ListController extends Component { const staticActionItems = Object.entries(this.getStaticActionMenuItems()) .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable()) .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0)) - .map(([key, item]) => Object.assign({ key }, omit(item, "isAvailable", "sequence"))); + .map(([key, item]) => + Object.assign( + { key, groupNumber: STATIC_ACTIONS_GROUP_NUMBER }, + omit(item, "isAvailable") + ) + ); return { action: [...staticActionItems, ...(actionMenus.action || [])], @@ -614,10 +621,7 @@ ListController.components = { ViewButton, MultiRecordViewButton, SearchBar, - Dropdown, - DropdownItem, CogMenu, - ActionMenusItems, }; ListController.props = { ...standardViewProps, diff --git a/addons/web/static/src/views/list/list_controller.xml b/addons/web/static/src/views/list/list_controller.xml index 4c036f9b536..d5fafe68e60 100644 --- a/addons/web/static/src/views/list/list_controller.xml +++ b/addons/web/static/src/views/list/list_controller.xml @@ -39,11 +39,7 @@ - - - Export All - - +
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml b/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml index d9c20b1987a..7b18e26850d 100644 --- a/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml +++ b/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml @@ -17,6 +17,7 @@ + initialApp diff --git a/addons/web/static/tests/search/helpers.js b/addons/web/static/tests/search/helpers.js index d7926281254..bd3be47e6f4 100644 --- a/addons/web/static/tests/search/helpers.js +++ b/addons/web/static/tests/search/helpers.js @@ -144,7 +144,7 @@ function getNode(target) { return target instanceof Component ? target.el : target; } -function findItem(target, selector, finder = 0) { +export function findItem(target, selector, finder = 0) { const el = getNode(target); const elems = [...el.querySelectorAll(selector)]; if (Number.isInteger(finder)) { diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 66eb51dbfc5..23ec0cb9a69 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -8057,7 +8057,7 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("display toolbar", async function (assert) { - assert.expect(7); + assert.expect(6); const actionService = { start() { @@ -8092,7 +8092,6 @@ QUnit.module("Views", (hooks) => { await toggleActionMenu(target); assert.containsNone(target, ".o_cp_action_menus .dropdown-menu_group:contains(Print)"); - assert.containsOnce(target, ".o_cp_action_menus .dropdown-menu_group:contains(Action)"); assert.containsN( target, ".o_cp_action_menus .dropdown-item", diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 930a7c11d25..625ec8fccce 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -573,7 +573,7 @@ QUnit.module("Views", (hooks) => { arch: '', }); - assert.containsOnce(target, "div.o_control_panel .o_cp_action_menus"); + assert.containsNone(target, "div.o_control_panel .o_cp_action_menus"); assert.ok( $(target).find("tbody td.o_list_record_selector").length, "should have at least one record" diff --git a/addons/website/static/src/components/views/page_list.js b/addons/website/static/src/components/views/page_list.js index 6744ba492ca..aa0441fa26e 100644 --- a/addons/website/static/src/components/views/page_list.js +++ b/addons/website/static/src/components/views/page_list.js @@ -38,6 +38,7 @@ export class PageListController extends PageControllerMixin(listView.Controller) const menuItems = super.getStaticActionMenuItems(); menuItems.publish = { sequence: 15, + icon: "fa fa-globe", description: this.env._t("Publish"), callback: async () => { this.dialogService.add(ConfirmationDialog, { @@ -54,6 +55,7 @@ export class PageListController extends PageControllerMixin(listView.Controller) }; menuItems.unpublish = { sequence: 16, + icon: "fa fa-chain-broken", description: this.env._t("Unpublish"), callback: async () => this.togglePublished(false), };