diff --git a/addons/web/static/src/core/pager/pager.js b/addons/web/static/src/core/pager/pager.js index 108c347332b..014a15ba832 100644 --- a/addons/web/static/src/core/pager/pager.js +++ b/addons/web/static/src/core/pager/pager.js @@ -50,7 +50,7 @@ export class Pager extends Component { return parts.join("-"); } /** - * @returns {boolean} true iff there is only one page + * @returns {boolean} true if there is only one page */ get isSinglePage() { return this.minimum === 1 && this.maximum === this.props.total; @@ -68,15 +68,14 @@ export class Pager extends Component { } else if (minimum < 0 && this.props.limit > 1) { minimum = this.props.total - (this.props.total % this.props.limit || this.props.limit); } - - this.update(minimum, this.props.limit); + this.update(minimum, this.props.limit, true); } /** * @param {string} value * @returns {{ minimum: number, maximum: number }} */ parse(value) { - let [minimum, maximum] = value.trim().split(/\s*[\-\s,;]\s*/); + let [minimum, maximum] = value.trim().split(/\s*[-\s,;]\s*/); const clamp = (value) => Math.min(Math.max(value, 1), this.props.total); return { minimum: clamp(parseInt(minimum, 10)) - 1, @@ -96,10 +95,11 @@ export class Pager extends Component { /** * @param {number} offset * @param {number} limit + * @param {Boolean} hasNavigated */ - async update(offset, limit) { + async update(offset, limit, hasNavigated) { this.state.isDisabled = true; - await this.props.onUpdate({ offset, limit }); + await this.props.onUpdate({ offset, limit }, hasNavigated); this.state.isDisabled = false; this.state.isEditing = false; } diff --git a/addons/web/static/src/search/action_menus/action_menus.js b/addons/web/static/src/search/action_menus/action_menus.js new file mode 100644 index 00000000000..f12d2b8d74e --- /dev/null +++ b/addons/web/static/src/search/action_menus/action_menus.js @@ -0,0 +1,145 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { makeContext } from "@web/core/context"; +import { session } from "@web/session"; +import { registry } from "@web/core/registry"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { useService } from "@web/core/utils/hooks"; + +const { Component, onWillStart, onWillUpdateProps } = owl; +let registryActionId = 0; +/** + * Action menus (or Action/Print bar, previously called 'Sidebar') + * + * 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 ActionMenus extends Component { + setup() { + this.orm = useService("orm"); + this.actionService = useService("action"); + onWillStart(async () => { + this.actionItems = await this.setActionItems(this.props); + }); + onWillUpdateProps(async (nextProps) => { + this.actionItems = await this.setActionItems(nextProps); + }); + } + + get printItems() { + const printActions = this.props.items.print || []; + return printActions.map((action) => ({ + action, + description: action.name, + key: action.id, + })); + } + + //--------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------- + + async setActionItems(props) { + // Callback based actions + const callbackActions = (props.items.other || []).map((action) => + Object.assign({ key: `action-${action.description}` }, action) + ); + // Action based actions + const actionActions = props.items.action || []; + const formattedActions = actionActions.map((action) => ({ + action, + description: action.name, + key: action.id, + })); + // ActionMenus action registry components + const registryActions = []; + for (const { Component, getProps } of registry.category("action_menus").getAll()) { + const itemProps = await getProps(props, this.env); + if (itemProps) { + registryActions.push({ + Component, + key: `registry-action-${registryActionId++}`, + props: itemProps, + }); + } + } + + return [...callbackActions, ...formattedActions, ...registryActions]; + } + + //--------------------------------------------------------------------- + // Handlers + //--------------------------------------------------------------------- + + async executeAction(action) { + let activeIds = this.props.activeIds; + if (this.props.isDomainSelected) { + activeIds = await this.orm.search(this.props.resModel, this.props.domain, { + limit: session.active_ids_limit, + context: this.props.context, + }); + } + const activeIdsContext = { + active_id: activeIds[0], + active_ids: activeIds, + active_model: this.props.resModel, + }; + if (this.props.domain) { + // keep active_domain in context for backward compatibility + // reasons, and to allow actions to bypass the active_ids_limit + activeIdsContext.active_domain = this.props.domain; + } + const context = makeContext([this.props.context, activeIdsContext]); + return this.actionService.doAction(action.id, { + additionalContext: context, + on_close: () => this.trigger("reload"), + }); + } + + /** + * Handler used to determine which way must be used to execute a selected + * action: it will be either: + * - a callback (function given by the view controller); + * - an action ID (string); + * - an URL (string). + * @private + * @param {Object} item + */ + onItemSelected(item) { + if (item.callback) { + item.callback([item]); + } else if (item.action) { + this.executeAction(item.action); + } else if (item.url) { + // Event has been prevented at its source: we need to redirect manually. + browser.location = item.url; + } + } +} + +ActionMenus.components = { + Dropdown, + DropdownItem, +}; +ActionMenus.props = { + activeIds: { type: Array, element: [Number, String] }, // virtual IDs are strings. + context: Object, + resModel: String, + domain: { type: Array, optional: true }, + isDomainSelected: { type: Boolean, optional: true }, + items: { + type: Object, + shape: { + action: { type: Array, optional: true }, + print: { type: Array, optional: true }, + other: { type: Array, optional: true }, + }, + }, +}; +ActionMenus.template = "web.ActionMenus"; diff --git a/addons/web/static/src/search/action_menus/action_menus.xml b/addons/web/static/src/search/action_menus/action_menus.xml new file mode 100644 index 00000000000..56ca0a56f9e --- /dev/null +++ b/addons/web/static/src/search/action_menus/action_menus.xml @@ -0,0 +1,33 @@ + + + + +
+ + + + Print + + + + + + + + + + + + Action + + + + + + + + +
+
+ +
diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js index 65ea3c45a4c..9db06294bdf 100644 --- a/addons/web/static/src/search/control_panel/control_panel.js +++ b/addons/web/static/src/search/control_panel/control_panel.js @@ -1,5 +1,6 @@ /** @odoo-module **/ +import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service"; import { Pager } from "@web/core/pager/pager"; import { useService } from "@web/core/utils/hooks"; import { ComparisonMenu } from "../comparison_menu/comparison_menu"; @@ -25,6 +26,18 @@ export class ControlPanel extends Component { : undefined; } + /** + * @param {KeyboardEvent} ev + */ + onBottomLeftKeydown(ev) { + const hotkey = getActiveHotkey(ev); + if (hotkey === "arrowdown") { + this.env.searchModel.trigger("focus-view"); + ev.preventDefault(); + ev.stopPropagation(); + } + } + /** * @returns {Object} */ @@ -34,6 +47,7 @@ export class ControlPanel extends Component { "top-left": true, "top-right": true, "bottom-left": true, + "bottom-left-buttons": true, "bottom-right": true, }, this.props.display || this.env.searchModel.display.controlPanel diff --git a/addons/web/static/src/search/control_panel/control_panel.xml b/addons/web/static/src/search/control_panel/control_panel.xml index 90424ed3314..60bff845c65 100644 --- a/addons/web/static/src/search/control_panel/control_panel.xml +++ b/addons/web/static/src/search/control_panel/control_panel.xml @@ -2,46 +2,49 @@ -
+
-
- - +
+ +
-
- +
+
-
- +
+ +
-
- - @@ -52,11 +55,7 @@