odoo.define('web.ControlPanel', function (require) { "use strict"; const ActionMenus = require('web.ActionMenus'); const ComparisonMenu = require('web.ComparisonMenu'); const ActionModel = require('web/static/src/js/views/action_model.js'); const FavoriteMenu = require('web.FavoriteMenu'); const FilterMenu = require('web.FilterMenu'); const GroupByMenu = require('web.GroupByMenu'); const patchMixin = require('web.patchMixin'); const Pager = require('web.Pager'); const SearchBar = require('web.SearchBar'); const { useModel } = require('web/static/src/js/model.js'); const { Component, hooks } = owl; const { useRef, useSubEnv } = hooks; /** * TODO: remove this whole mechanism as soon as `cp_content` is completely removed. * Extract the 'cp_content' key of the given props and return them as well as * the extracted content. * @param {Object} props * @returns {Object} */ function getAdditionalContent(props) { const additionalContent = {}; if ('cp_content' in props) { const content = props.cp_content || {}; if ('$buttons' in content) { additionalContent.buttons = content.$buttons; } if ('$searchview' in content) { additionalContent.searchView = content.$searchview; } if ('$pager' in content) { additionalContent.pager = content.$pager; } if ('$searchview_buttons' in content) { additionalContent.searchViewButtons = content.$searchview_buttons; } } return additionalContent; } /** * Control panel * * The control panel of the action|view. In its standard form, it is composed of * several sections/subcomponents. Here is a simplified graph representing the * action|view and its control panel: * * ┌ View Controller | Action ----------------------------------------------------------┐ * | ┌ Control Panel ──────────────┬──────────────────────────────────────────────────┐ | * | │ ┌ Breadcrumbs ────────────┐ │ ┌ Search View ─────────────────────────────────┐ │ | * | │ │ [1] / [2] │ │ │ [3] [ ================ 4 ================= ] │ │ | * | │ └─────────────────────────┘ │ └──────────────────────────────────────────────┘ │ | * | ├─────────────────────────────┼──────────────────────────────────────────────────┤ | * | │ ┌ Buttons ┐ ┌ ActionMenus ┐ │ ┌ Search Menus ─────┐ ┌ Pager ┐┌ View switcher ┐ │ | * | │ │ [5] │ │ [6] │ │ │ [7] [8] [9] [10] │ │ [11] ││ [12] │ │ | * | │ └─────────┘ └─────────────┘ │ └───────────────────┘ └───────┘└───────────────┘ │ | * | └─────────────────────────────┴──────────────────────────────────────────────────┘ | * | ┌ View Renderer | Action content ────────────────────────────────────────────────┐ | * | │ │ | * | │ ... │ | * | │ │ | * | │ │ | * | │ │ | * | └────────────────────────────────────────────────────────────────────────────────┘ | * └------------------------------------------------------------------------------------┘ * * 1. Breadcrumbs: list of links composed by the `props.breadcrumbs` collection. * 2. Title: the title of the action|view. Can be empty and will yield 'Unnamed'. * 3. Search facets: a collection of facet components generated by the `ControlPanelModel` * and handled by the `SearchBar` component. @see SearchFacet * 4. SearchBar: @see SearchBar * 5. Buttons: section in which the action|controller is meant to inject its control * buttons. The template provides a slot for this purpose. * 6. Action menus: @see ActionMenus * 7. Filter menu: @see FilterMenu * 8. Group by menu: @see GroupByMenu * 9. Comparison menu: @see ComparisonMenu * 10. Favorite menu: @see FavoriteMenu * 11. Pager: @see Pager * 12. View switcher buttons: list of buttons composed by the `props.views` collection. * * Subcomponents (especially in the [Search Menus] section) will call * the ControlPanelModel to get processed information about the current view|action. * @see ControlPanelModel for more details. * * Note: an additional temporary (and ugly) mechanic allows to inject a jQuery element * given in `props.cp_content` in a related section: * $buttons -> [Buttons] * $searchview -> [Search View] * $searchview_buttons -> [Search Menus] * $pager -> [Pager] * This system must be replaced by proper slot usage and the static template * inheritance mechanism when converting the views/actions. * @extends Component */ class ControlPanel extends Component { constructor() { super(...arguments); this.additionalContent = getAdditionalContent(this.props); useSubEnv({ action: this.props.action, searchModel: this.props.searchModel, view: this.props.view, }); // Connect to the model // TODO: move this in enterprise whenever possible if (this.env.searchModel) { this.model = useModel('searchModel'); } // Reference hooks this.contentRefs = { buttons: useRef('buttons'), pager: useRef('pager'), searchView: useRef('searchView'), searchViewButtons: useRef('searchViewButtons'), }; this.fields = this._formatFields(this.props.fields); this.sprintf = _.str.sprintf; } mounted() { this._attachAdditionalContent(); } patched() { this._attachAdditionalContent(); } async willUpdateProps(nextProps) { // Note: action and searchModel are not likely to change during // the lifespan of a ControlPanel instance, so we only need to update // the view information. if ('view' in nextProps) { this.env.view = nextProps.view; } if ('fields' in nextProps) { this.fields = this._formatFields(nextProps.fields); } this.additionalContent = getAdditionalContent(nextProps); } //--------------------------------------------------------------------- // Private //--------------------------------------------------------------------- /** * Attach additional content extracted from the props 'cp_content' key, if any. * @private */ _attachAdditionalContent() { for (const key in this.additionalContent) { if (this.additionalContent[key] && this.additionalContent[key].length) { const target = this.contentRefs[key].el; if (target) { target.innerHTML = ""; target.append(...this.additionalContent[key]); } } } } /** * Give `name` and `description` keys to the fields given to the control * panel. * @private * @param {Object} fields * @returns {Object} */ _formatFields(fields) { const formattedFields = {}; for (const fieldName in fields) { formattedFields[fieldName] = Object.assign({ description: fields[fieldName].string, name: fieldName, }, fields[fieldName]); } return formattedFields; } } ControlPanel.modelExtension = "ControlPanel"; ControlPanel.components = { SearchBar, ActionMenus, Pager, ComparisonMenu, FilterMenu, GroupByMenu, FavoriteMenu, }; ControlPanel.defaultProps = { breadcrumbs: [], fields: {}, searchMenuTypes: [], views: [], withBreadcrumbs: true, withSearchBar: true, }; ControlPanel.props = { action: Object, breadcrumbs: Array, searchModel: ActionModel, cp_content: { type: Object, optional: 1 }, fields: Object, pager: { validate: p => typeof p === 'object' || p === null, optional: 1 }, searchMenuTypes: Array, actionMenus: { validate: s => typeof s === 'object' || s === null, optional: 1 }, title: { type: String, optional: 1 }, view: { type: Object, optional: 1 }, views: Array, withBreadcrumbs: Boolean, withSearchBar: Boolean, }; ControlPanel.template = 'web.ControlPanel'; return patchMixin(ControlPanel); });