diff --git a/addons/base_import/static/src/js/import_menu.js b/addons/base_import/static/src/js/import_menu.js index a5c5c5e1293..e5442fac10c 100644 --- a/addons/base_import/static/src/js/import_menu.js +++ b/addons/base_import/static/src/js/import_menu.js @@ -3,7 +3,7 @@ odoo.define('base_import.ImportMenu', function (require) { const DropdownMenuItem = require('web.DropdownMenuItem'); const FavoriteMenu = require('web.FavoriteMenu'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * Import Records menu @@ -15,7 +15,7 @@ odoo.define('base_import.ImportMenu', function (require) { class ImportMenu extends DropdownMenuItem { constructor() { super(...arguments); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); } //--------------------------------------------------------------------- @@ -30,8 +30,8 @@ odoo.define('base_import.ImportMenu', function (require) { type: 'ir.actions.client', tag: 'import', params: { - model: this.model.modelName, - context: this.model.actionContext, + model: this.model.config.modelName, + context: this.model.config.context, } }; this.trigger('do-action', {action: action}); diff --git a/addons/board/static/src/js/add_to_board_menu.js b/addons/board/static/src/js/add_to_board_menu.js index 95e15e530c2..709620df156 100644 --- a/addons/board/static/src/js/add_to_board_menu.js +++ b/addons/board/static/src/js/add_to_board_menu.js @@ -5,7 +5,6 @@ odoo.define('board.AddToBoardMenu', function (require) { const Domain = require('web.Domain'); const DropdownMenuItem = require('web.DropdownMenuItem'); const FavoriteMenu = require('web.FavoriteMenu'); - const pyUtils = require('web.py_utils'); const { sprintf } = require('web.utils'); const { useAutofocus } = require('web.custom_hooks'); @@ -48,7 +47,7 @@ odoo.define('board.AddToBoardMenu', function (require) { * @private */ async _addToBoard() { - const searchQuery = this.env.controlPanelModel.getQuery(); + const searchQuery = this.env.searchModel.get('query'); const context = new Context(this.env.action.context); context.add(searchQuery.context); context.add({ @@ -61,7 +60,7 @@ odoo.define('board.AddToBoardMenu', function (require) { }); } let controllerQueryParams; - this.env.controlPanelModel.trigger('get-controller-query-params', params => { + this.env.searchModel.trigger('get-controller-query-params', params => { controllerQueryParams = params || {}; }); controllerQueryParams.context = controllerQueryParams.context || {}; diff --git a/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js b/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js index 4d8ac9552cc..d8d19a06148 100644 --- a/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js +++ b/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js @@ -23,7 +23,7 @@ odoo.define('board.AddToGoogleSpreadsheetMenu', function (require) { * @private */ async _onAddToSpreadsheet() { - const searchQuery = this.env.controlPanelModel.getQuery(); + const searchQuery = this.env.model.get('query'); const listView = this.env.action.views.find(view => view.type === 'list'); const modelName = this.env.action.res_model; const domain = Domain.prototype.arrayToString(searchQuery.domain); diff --git a/addons/mail/static/src/widgets/discuss/discuss.js b/addons/mail/static/src/widgets/discuss/discuss.js index 6f55834895f..2537a4d6513 100644 --- a/addons/mail/static/src/widgets/discuss/discuss.js +++ b/addons/mail/static/src/widgets/discuss/discuss.js @@ -49,7 +49,7 @@ const DiscussWidget = AbstractAction.extend({ // control panel attributes this.action = action; this.actionManager = parent; - this.controlPanelModelConfig.modelName = 'mail.message'; + this.searchModelConfig.modelName = 'mail.message'; this.discuss = undefined; this.options = options; diff --git a/addons/web/static/src/js/chrome/abstract_action.js b/addons/web/static/src/js/chrome/abstract_action.js index 982b750ab56..fc5c1fd4873 100644 --- a/addons/web/static/src/js/chrome/abstract_action.js +++ b/addons/web/static/src/js/chrome/abstract_action.js @@ -9,8 +9,8 @@ odoo.define('web.AbstractAction', function (require) { */ var ActionMixin = require('web.ActionMixin'); +const ActionModel = require('web/static/src/js/views/action_model.js'); var ControlPanel = require('web.ControlPanel'); -var ControlPanelModel = require('web.ControlPanelModel'); var Widget = require('web.Widget'); const { ComponentWrapper } = require('web.OwlCompatibility'); @@ -35,7 +35,7 @@ var AbstractAction = Widget.extend(ActionMixin, { * For example, the Discuss application adds the following line in its * constructor:: * - * this.controlPanelModelConfig.modelName = 'mail.message'; + * this.searchModelConfig.modelName = 'mail.message'; * * @type boolean */ @@ -76,12 +76,16 @@ var AbstractAction = Widget.extend(ActionMixin, { this._super(parent); this._title = action.display_name || action.name; + this.searchModelConfig = { + context: Object.assign({}, action.context), + domain: action.domain || [], + env: owl.Component.env, + searchMenuTypes: this.searchMenuTypes, + }; + this.extensions = {}; if (this.hasControlPanel) { - this.controlPanelModelConfig = { + this.extensions.ControlPanel = { actionId: action.id, - actionContext: action.context || {}, - actionDomain: action.domain || [], - env: owl.Component.env, withSearchBar: this.withSearchBar, }; @@ -102,23 +106,36 @@ var AbstractAction = Widget.extend(ActionMixin, { * @override */ willStart: async function () { - const proms = [this._super(...arguments)]; + const superPromise = this._super(...arguments); if (this.hasControlPanel) { if (this.loadControlPanel) { - const { context, searchMenuTypes } = this.controlPanelProps; - const { modelName } = this.controlPanelModelConfig; - const options = { load_filters: searchMenuTypes.includes('favorite') }; - const args = [modelName, context || {}, this.viewId, 'search', options]; - const loadFieldViewPromise = this.loadFieldView(...args); - const {arch, fields, favoriteFilters } = await loadFieldViewPromise; - this.controlPanelModelConfig.viewInfo = {arch, fields, favoriteFilters }; + const { context, modelName } = this.searchModelConfig; + const options = { load_filters: this.searchMenuTypes.includes('favorite') }; + const { arch, fields, favoriteFilters } = await this.loadFieldView( + modelName, + context || {}, + this.viewId, + 'search', + options + ); + const archs = { search: arch }; + const { ControlPanel: controlPanelInfo } = ActionModel.extractArchInfo(archs); + Object.assign(this.extensions.ControlPanel, { + archNodes: controlPanelInfo.children, + favoriteFilters, + fields, + }); this.controlPanelProps.fields = fields; } - this._controlPanelModel = new ControlPanelModel(this.controlPanelModelConfig); - this.controlPanelProps.controlPanelModel = this._controlPanelModel; - proms.push(this._controlPanelModel.isReady); } - return Promise.all(proms); + this.searchModel = new ActionModel(this.extensions, this.searchModelConfig); + if (this.hasControlPanel) { + this.controlPanelProps.searchModel = this.searchModel; + } + return Promise.all([ + superPromise, + this.searchModel.load(), + ]); }, /** * @override @@ -147,9 +164,9 @@ var AbstractAction = Widget.extend(ActionMixin, { */ on_attach_callback: function () { ActionMixin.on_attach_callback.call(this); + this.searchModel.on('search', this, this._onSearch); if (this.hasControlPanel) { - this._controlPanelModel.on('search', this, this._onSearch); - this._controlPanelModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams); + this.searchModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams); } }, /** @@ -157,9 +174,9 @@ var AbstractAction = Widget.extend(ActionMixin, { */ on_detach_callback: function () { ActionMixin.on_detach_callback.call(this); + this.searchModel.off('search', this); if (this.hasControlPanel) { - this._controlPanelModel.off('search', this); - this._controlPanelModel.off('get-controller-query-params', this); + this.searchModel.off('get-controller-query-params', this); } }, diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index f6c5661eb8e..749feacb3d6 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -488,6 +488,7 @@ ActionManager.include({ // can't switch to an unknown view return Promise.reject(); } + var currentController = this.getCurrentController(); var index; if (currentController.actionID !== action.jsID) { @@ -546,7 +547,7 @@ ActionManager.include({ return Promise.resolve(controller.widget.willRestore()).then(function () { viewOptions = _.extend({}, viewOptions, { breadcrumbs: self._getBreadcrumbs(self.controllerStack.slice(0, index)), - shouldUpdateControlPanel: true, + shouldUpdateSearchComponents: true, }); return controller.widget.reload(viewOptions).then(function () { return controller; diff --git a/addons/web/static/src/js/control_panel/comparison_menu.js b/addons/web/static/src/js/control_panel/comparison_menu.js index 4504370e7c4..dee1c982d2e 100644 --- a/addons/web/static/src/js/control_panel/comparison_menu.js +++ b/addons/web/static/src/js/control_panel/comparison_menu.js @@ -3,7 +3,7 @@ odoo.define("web.ComparisonMenu", function (require) { const DropdownMenu = require("web.DropdownMenu"); const { FACET_ICONS } = require("web.searchUtils"); - const { useModel } = require("web.model"); + const { useModel } = require("web/static/src/js/model.js"); /** * "Comparison" menu @@ -15,7 +15,7 @@ odoo.define("web.ComparisonMenu", function (require) { class ComparisonMenu extends DropdownMenu { constructor() { super(...arguments); - this.model = useModel("controlPanelModel"); + this.model = useModel('searchModel'); } //--------------------------------------------------------------------- @@ -33,7 +33,7 @@ odoo.define("web.ComparisonMenu", function (require) { * @override */ get items() { - return this.model.getFiltersOfType("comparison"); + return this.model.get('filters', f => f.type === 'comparison'); } /** diff --git a/addons/web/static/src/js/control_panel/control_panel.js b/addons/web/static/src/js/control_panel/control_panel.js index b315893076a..aca28bc0eb2 100644 --- a/addons/web/static/src/js/control_panel/control_panel.js +++ b/addons/web/static/src/js/control_panel/control_panel.js @@ -2,15 +2,15 @@ odoo.define('web.ControlPanel', function (require) { "use strict"; const ActionMenus = require('web.ActionMenus'); - const ControlPanelModel = require('web.ControlPanelModel'); 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.model'); + const { useModel } = require('web/static/src/js/model.js'); const { Component, hooks } = owl; const { useRef, useSubEnv } = hooks; @@ -105,14 +105,14 @@ odoo.define('web.ControlPanel', function (require) { useSubEnv({ action: this.props.action, - controlPanelModel: this.props.controlPanelModel, + searchModel: this.props.searchModel, view: this.props.view, }); // Connect to the model // TODO: move this in enterprise whenever possible - if (this.env.controlPanelModel) { - this.model = useModel('controlPanelModel'); + if (this.env.searchModel) { + this.model = useModel('searchModel'); } // Reference hooks @@ -135,7 +135,7 @@ odoo.define('web.ControlPanel', function (require) { } async willUpdateProps(nextProps) { - // Note: action and controlPanelModel are not likely to change during + // 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) { @@ -185,6 +185,7 @@ odoo.define('web.ControlPanel', function (require) { return formattedFields; } } + ControlPanel.modelExtension = "ControlPanel"; ControlPanel.components = { SearchBar, @@ -202,7 +203,7 @@ odoo.define('web.ControlPanel', function (require) { ControlPanel.props = { action: Object, breadcrumbs: Array, - controlPanelModel: ControlPanelModel, + searchModel: ActionModel, cp_content: { type: Object, optional: 1 }, fields: Object, pager: { validate: p => typeof p === 'object' || p === null, optional: 1 }, @@ -214,7 +215,6 @@ odoo.define('web.ControlPanel', function (require) { withBreadcrumbs: Boolean, withSearchBar: Boolean, }; - ControlPanel.template = 'web.ControlPanel'; return patchMixin(ControlPanel); diff --git a/addons/web/static/src/js/control_panel/custom_favorite_item.js b/addons/web/static/src/js/control_panel/custom_favorite_item.js index b7b3a34ef81..b7adb031467 100644 --- a/addons/web/static/src/js/control_panel/custom_favorite_item.js +++ b/addons/web/static/src/js/control_panel/custom_favorite_item.js @@ -4,7 +4,7 @@ odoo.define('web.CustomFavoriteItem', function (require) { const DropdownMenuItem = require('web.DropdownMenuItem'); const FavoriteMenu = require('web.FavoriteMenu'); const { useAutofocus } = require('web.custom_hooks'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); const { useRef } = owl.hooks; @@ -39,7 +39,7 @@ odoo.define('web.CustomFavoriteItem', function (require) { this.shareAllUsersId = `o_favorite_share_all_users_${favId}`; this.descriptionRef = useRef('description'); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); this.interactive = true; Object.assign(this.state, { description: this.env.action.name || "", @@ -65,7 +65,7 @@ odoo.define('web.CustomFavoriteItem', function (require) { }); return this.descriptionRef.el.focus(); } - const favorites = this.model.getFiltersOfType('favorite'); + const favorites = this.model.get('filters', f => f.type === 'favorite'); if (favorites.some(f => f.description === this.state.description)) { this.env.services.notification.notify({ message: this.env._t("Filter with same name already exists."), diff --git a/addons/web/static/src/js/control_panel/custom_filter_item.js b/addons/web/static/src/js/control_panel/custom_filter_item.js index 592a948d135..98080f6a418 100644 --- a/addons/web/static/src/js/control_panel/custom_filter_item.js +++ b/addons/web/static/src/js/control_panel/custom_filter_item.js @@ -7,7 +7,7 @@ odoo.define('web.CustomFilterItem', function (require) { const { FIELD_OPERATORS, FIELD_TYPES } = require('web.searchUtils'); const field_utils = require('web.field_utils'); const patchMixin = require('web.patchMixin'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * Filter generator menu @@ -46,7 +46,7 @@ odoo.define('web.CustomFilterItem', function (require) { constructor() { super(...arguments); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); this.canBeOpened = true; this.state.conditions = []; diff --git a/addons/web/static/src/js/control_panel/custom_group_by_item.js b/addons/web/static/src/js/control_panel/custom_group_by_item.js index 8082608d3ff..20a1c3c7eb7 100644 --- a/addons/web/static/src/js/control_panel/custom_group_by_item.js +++ b/addons/web/static/src/js/control_panel/custom_group_by_item.js @@ -2,7 +2,7 @@ odoo.define('web.CustomGroupByItem', function (require) { "use strict"; const DropdownMenuItem = require('web.DropdownMenuItem'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * Group by generator menu @@ -20,7 +20,7 @@ odoo.define('web.CustomGroupByItem', function (require) { this.canBeOpened = true; this.state.fieldName = this.props.fields[0].name; - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); } //--------------------------------------------------------------------- diff --git a/addons/web/static/src/js/control_panel/favorite_menu.js b/addons/web/static/src/js/control_panel/favorite_menu.js index 066b30f30d5..ff09109fe59 100644 --- a/addons/web/static/src/js/control_panel/favorite_menu.js +++ b/addons/web/static/src/js/control_panel/favorite_menu.js @@ -5,7 +5,7 @@ odoo.define('web.FavoriteMenu', function (require) { const DropdownMenu = require('web.DropdownMenu'); const { FACET_ICONS } = require("web.searchUtils"); const Registry = require('web.Registry'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * 'Favorites' menu @@ -22,7 +22,7 @@ odoo.define('web.FavoriteMenu', function (require) { constructor() { super(...arguments); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); this.state.deletedFavorite = false; } @@ -41,7 +41,7 @@ odoo.define('web.FavoriteMenu', function (require) { * @override */ get items() { - const favorites = this.model.getFiltersOfType('favorite'); + const favorites = this.model.get('filters', f => f.type === 'favorite'); const registryMenus = this.constructor.registry.values().reduce( (menus, Component) => { if (Component.shouldBeDisplayed(this.env)) { @@ -91,7 +91,7 @@ odoo.define('web.FavoriteMenu', function (require) { * @private */ async _onRemoveFavorite() { - await this.model.dispatch('deleteFavorite', this.state.deletedFavorite.id); + this.model.dispatch('deleteFavorite', this.state.deletedFavorite.id); this.state.deletedFavorite = false; } } diff --git a/addons/web/static/src/js/control_panel/filter_menu.js b/addons/web/static/src/js/control_panel/filter_menu.js index 2341def75b0..da37eb2359b 100644 --- a/addons/web/static/src/js/control_panel/filter_menu.js +++ b/addons/web/static/src/js/control_panel/filter_menu.js @@ -4,7 +4,7 @@ odoo.define('web.FilterMenu', function (require) { const CustomFilterItem = require('web.CustomFilterItem'); const DropdownMenu = require('web.DropdownMenu'); const { FACET_ICONS } = require("web.searchUtils"); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * 'Filters' menu @@ -20,7 +20,7 @@ odoo.define('web.FilterMenu', function (require) { constructor() { super(...arguments); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); } //--------------------------------------------------------------------- @@ -38,7 +38,7 @@ odoo.define('web.FilterMenu', function (require) { * @override */ get items() { - return this.model.getFiltersOfType('filter'); + return this.model.get('filters', f => f.type === 'filter'); } /** diff --git a/addons/web/static/src/js/control_panel/groupby_menu.js b/addons/web/static/src/js/control_panel/groupby_menu.js index 97bad1a2ce7..546952ad50d 100644 --- a/addons/web/static/src/js/control_panel/groupby_menu.js +++ b/addons/web/static/src/js/control_panel/groupby_menu.js @@ -4,7 +4,7 @@ odoo.define('web.GroupByMenu', function (require) { const CustomGroupByItem = require('web.CustomGroupByItem'); const DropdownMenu = require('web.DropdownMenu'); const { FACET_ICONS, GROUPABLE_TYPES } = require('web.searchUtils'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); /** * 'Group by' menu @@ -24,7 +24,7 @@ odoo.define('web.GroupByMenu', function (require) { .filter(field => this._validateField(field)) .sort(({ string: a }, { string: b }) => a > b ? 1 : a < b ? -1 : 0); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); } //--------------------------------------------------------------------- @@ -42,7 +42,7 @@ odoo.define('web.GroupByMenu', function (require) { * @override */ get items() { - return this.model.getFiltersOfType('groupBy'); + return this.model.get('filters', f => f.type === 'groupBy'); } /** diff --git a/addons/web/static/src/js/control_panel/search_bar.js b/addons/web/static/src/js/control_panel/search_bar.js index 3082c38c055..22f6ee6a60b 100644 --- a/addons/web/static/src/js/control_panel/search_bar.js +++ b/addons/web/static/src/js/control_panel/search_bar.js @@ -4,7 +4,7 @@ odoo.define('web.SearchBar', function (require) { const Domain = require('web.Domain'); const field_utils = require('web.field_utils'); const { useAutofocus } = require('web.custom_hooks'); - const { useModel } = require('web.model'); + const { useModel } = require('web/static/src/js/model.js'); const CHAR_FIELDS = ['char', 'html', 'many2many', 'many2one', 'one2many', 'text']; const { Component, hooks } = owl; @@ -42,14 +42,14 @@ odoo.define('web.SearchBar', function (require) { this.focusOnUpdate = useAutofocus(); this.inputRef = useRef('search-input'); - this.model = useModel('controlPanelModel'); + this.model = useModel('searchModel'); this.state = useState({ sources: [], focusedItem: 0, inputValue: "", }); - this.autoCompleteSources = this.model.getFiltersOfType('field').map( + this.autoCompleteSources = this.model.get('filters', f => f.type === 'field').map( filter => this._createSource(filter) ); this.noResultItem = [null, this.env._t("(no result)")]; @@ -60,15 +60,15 @@ odoo.define('web.SearchBar', function (require) { mounted() { // 'search' will always patch the search bar, 'focus' will never. - this.env.controlPanelModel.on('search', this, this.focusOnUpdate); - this.env.controlPanelModel.on('focus-control-panel', this, () => { + this.env.searchModel.on('search', this, this.focusOnUpdate); + this.env.searchModel.on('focus-control-panel', this, () => { this.inputRef.el.focus(); }); } willUnmount() { - this.env.controlPanelModel.off('search', this); - this.env.controlPanelModel.off('focus-control-panel', this); + this.env.searchModel.off('search', this); + this.env.searchModel.off('focus-control-panel', this); } //--------------------------------------------------------------------- @@ -376,9 +376,8 @@ odoo.define('web.SearchBar', function (require) { this.state.focusedItem = this.state.sources.indexOf(currentItem.parent); // Priority 3: Do nothing (navigation inside text). } else if (ev.target.selectionStart === 0) { - const facets = this.model.getFacets(); // Priority 4: navigate to rightmost facet. - this._focusFacet(facets.length - 1); + this._focusFacet(this.model.get("facets").length - 1); } break; case 'ArrowRight': @@ -408,7 +407,7 @@ odoo.define('web.SearchBar', function (require) { break; case 'Backspace': if (!this.state.inputValue.length) { - const facets = this.model.getFacets(); + const facets = this.model.get("facets"); if (facets.length) { this._onFacetRemove(facets[facets.length - 1]); } diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 8a48c201fa6..5128fa6d9a8 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -16,7 +16,6 @@ var ActionMixin = require('web.ActionMixin'); var ajax = require('web.ajax'); var concurrency = require('web.concurrency'); const { ComponentWrapper } = require('web.OwlCompatibility'); -const ControlPanel = require('web.ControlPanel'); var mvc = require('web.mvc'); var session = require('web.session'); @@ -25,7 +24,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { custom_events: _.extend({}, ActionMixin.custom_events, { navigation_move: '_onNavigationMove', open_record: '_onOpenRecord', - search_panel_domain_updated: '_onSearchPanelDomainUpdated', switch_view: '_onSwitchView', }), events: { @@ -37,14 +35,13 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { * @param {Object[]} params.actionViews * @param {string} params.activeActions * @param {string} params.bannerRoute - * @param {Array[]} params.controlPanelDomain - * @param {ControlPanelModel} [params.controlPanelModel] - * @param {Object} [params.controlPanelProps] + * @param {Object} [params.controlPanel] * @param {string} params.controllerID an id to ease the communication with * upstream components * @param {string} params.displayName * @param {Object} params.initialState * @param {string} params.modelName + * @param {ActionModel} [params.searchModel] * @param {string} [params.searchPanel] * @param {string} params.viewType * @param {boolean} [params.withControlPanel] @@ -64,18 +61,20 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { this.dp = new concurrency.DropPrevious(); this.withControlPanel = params.withControlPanel; + this.withSearchPanel = params.withSearchPanel; + if (params.searchModel) { + this.searchModel = params.searchModel; + } if (this.withControlPanel) { - this.controlPanelProps = params.controlPanelProps; - this._controlPanelModel = params.controlPanelModel; + const { Component, props } = params.controlPanel; + this.ControlPanel = Component; + this.controlPanelProps = props; } - - this.withSearchPanel = params.withSearchPanel && params.searchPanel; - // the following attributes are used when there is a searchPanel if (this.withSearchPanel) { - this._searchPanel = params.searchPanel; + const { Component, props } = params.searchPanel; + this.SearchPanel = Component; + this.searchPanelProps = props; } - this.controlPanelDomain = params.controlPanelDomain || []; - this.searchPanelDomain = this._searchPanel ? this._searchPanel.getDomain() : []; }, /** @@ -84,23 +83,23 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { * @returns {Promise} */ start: async function () { - if (this.withSearchPanel) { - this.$('.o_content') - .addClass('o_controller_with_searchpanel') - .prepend(this._searchPanel.$el); - } this.$el.addClass('o_view_controller'); - this.renderButtons(); const promises = [this._super(...arguments)]; if (this.withControlPanel) { this._updateControlPanelProps(this.initialState); - this._controlPanelWrapper = new ComponentWrapper(this, ControlPanel, this.controlPanelProps); + this._controlPanelWrapper = new ComponentWrapper(this, this.ControlPanel, this.controlPanelProps); this._controlPanelWrapper.env.bus.on('focus-view', this, () => this._giveFocus()); promises.push(this._controlPanelWrapper.mount(this.el, { position: 'first-child' })); } + if (this.withSearchPanel) { + this._searchPanelWrapper = new ComponentWrapper(this, this.SearchPanel, this.searchPanelProps); + const content = this.el.querySelector(':scope .o_content'); + content.classList.add('o_controller_with_searchpanel'); + promises.push(this._searchPanelWrapper.mount(content, { position: 'first-child' })); + } await Promise.all(promises); - await this._update(this.initialState, { shouldUpdateControlPanel: false }); + await this._update(this.initialState, { shouldUpdateSearchComponents: false }); this.updateButtons(); }, /** @@ -118,12 +117,9 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { */ on_attach_callback: function () { ActionMixin.on_attach_callback.call(this); - if (this.withSearchPanel) { - this._searchPanel.on_attach_callback(); - } + this.searchModel.on('search', this, this._onSearch); if (this.withControlPanel) { - this._controlPanelModel.on('search', this, this._onSearch); - this._controlPanelModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams); + this.searchModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams); } if (!(this.renderer instanceof owl.Component)) { this.renderer.on_attach_callback(); @@ -134,9 +130,9 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { */ on_detach_callback: function () { ActionMixin.on_detach_callback.call(this); + this.searchModel.off('search', this); if (this.withControlPanel) { - this._controlPanelModel.off('search', this); - this._controlPanelModel.off('get-controller-query-params', this); + this.searchModel.off('get-controller-query-params', this); } if (!(this.renderer instanceof owl.Component)) { this.renderer.on_detach_callback(); @@ -177,20 +173,32 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { }, /** * Export the state of the controller containing information that is shared - * between different controllers of a same action (like the current - * searchQuery of the controlPanel). + * between different controllers of a same action (like the current search + * model state or the states of some components). * * @returns {Object} */ - exportState: function () { - var state = {}; - if (this.withControlPanel) { - state.cpState = this._controlPanelModel.exportState(); - } + exportState() { + const exported = { + searchModel: this.searchModel.exportState(), + }; if (this.withSearchPanel) { - state.spState = this._searchPanel.exportState(); + const searchPanel = this._searchPanelWrapper.componentRef.comp; + exported.searchPanel = searchPanel.exportState(); + } + return exported; + }, + /** + * Parses and imports a previously exported state. + * + * @param {Object} state + */ + importState(state) { + this.searchModel.importState(state.searchModel); + if (this.withSearchPanel) { + const searchPanel = this._searchPanelWrapper.componentRef.comp; + searchPanel.importState(state.searchPanel); } - return state; }, /** * The use of this method is discouraged. It is still snakecased, because @@ -207,40 +215,17 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { /** * Short helper method to reload the view * - * @param {Object} [params={}] This object will simply be given to the update + * @param {Object} [params={}] + * @param {Object} [params.controllerState={}] * @returns {Promise} */ reload: async function (params = {}) { - const controllerState = params.controllerState || {}; - const cpState = controllerState.cpState; - if (this.withControlPanel && cpState) { - this._controlPanelModel.importState(cpState); - const searchQuery = this._controlPanelModel.getQuery(); - params = Object.assign({}, params, searchQuery); - } - let postponeRendering = false; - if (this.withSearchPanel) { - if (params.domain) { - this.controlPanelDomain = params.domain; - } - if (controllerState.spState) { - this._searchPanel.importState(controllerState.spState); - } else { - const viewDomain = await this._getViewDomain(); - await this._searchPanel.update({ - searchDomain: this.controlPanelDomain, - viewDomain, - }); - postponeRendering = !params.noRender; - params.noRender = true; // wait for searchpanel to be ready to render - } - this.searchPanelDomain = this._searchPanel.getDomain(); - params.domain = this.controlPanelDomain.concat(this.searchPanelDomain); - } - await this.update(params, {}); - if (postponeRendering) { - return this._updateRendererState(false); + if (params.controllerState) { + this.importState(params.controllerState); + await this.searchModel.load(); + Object.assign(params, this.searchModel.get('query')); } + return this.update(params, {}); }, /** * This is the main entry point for the controller. Changes from the search @@ -287,15 +272,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { _getPagingInfo: function (state) { return null; }, - /** - * Get the domain defined by the view. It is meant to be overridden. - * - * @private - * @returns {Promise} - */ - _getViewDomain: async function () { - return []; - }, /** * Meant to be overriden to return a proper object. * @private @@ -409,7 +385,8 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { * @private * @param {Object} state the state given by the model * @param {Object} [params={}] - * @param {Object} [params.shouldUpdateControlPanel] + * @param {Array} [params.breadcrumbs] + * @param {Object} [params.shouldUpdateSearchComponents] * @returns {Promise} */ async _update(state, params) { @@ -418,12 +395,17 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { this.renderButtons(); } const promises = [this._renderBanner()]; - if (this.withControlPanel && params.shouldUpdateControlPanel !== false) { - this._updateControlPanelProps(state); - if (params.breadcrumbs) { - this.controlPanelProps.breadcrumbs = params.breadcrumbs; + if (params.shouldUpdateSearchComponents !== false) { + if (this.withControlPanel) { + this._updateControlPanelProps(state); + if (params.breadcrumbs) { + this.controlPanelProps.breadcrumbs = params.breadcrumbs; + } + promises.push(this.updateControlPanel()); + } + if (this.withSearchPanel) { + this._updateSearchPanel(); } - promises.push(this.updateControlPanel()); } this._pushState(); await Promise.all(promises); @@ -476,6 +458,15 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { } return this.renderer.updateState(state, params); }, + /** + * @private + * @param {Object} [newProps={}] + * @return {Promise} + */ + async _updateSearchPanel(newProps) { + Object.assign(this.searchPanelProps, newProps); + await this._searchPanelWrapper.update(this.searchPanelProps); + }, //-------------------------------------------------------------------------- // Handlers @@ -551,7 +542,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { switch (ev.data.direction) { case 'up': ev.stopPropagation(); - this._controlPanelModel.trigger('focus-control-panel'); + this.searchModel.trigger('focus-control-panel'); break; case 'down': ev.stopPropagation(); @@ -593,15 +584,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { _onSearch: function (searchQuery) { this.reload(_.extend({ offset: 0, groupsOffset: 0 }, searchQuery)); }, - /** - * @private - * @param {OdooEvent} ev - * @param {Array[]} ev.data.domain the current domain of the searchPanel - */ - _onSearchPanelDomainUpdated: function (ev) { - this.searchPanelDomain = ev.data.domain; - this.reload({offset: 0}); - }, /** * Intercepts the 'switch_view' event to add the controllerID into the data, * and lets the event bubble up. diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 6504314ddd1..04b1b781848 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -23,12 +23,13 @@ odoo.define('web.AbstractView', function (require) { * in most case discarded. */ +const ActionModel = require("web/static/src/js/views/action_model.js"); var AbstractModel = require('web.AbstractModel'); var AbstractRenderer = require('web.AbstractRenderer'); var AbstractController = require('web.AbstractController'); -const ControlPanelModel = require('web.ControlPanelModel'); +const ControlPanel = require('web.ControlPanel'); +const SearchPanel = require("web/static/src/js/views/search_panel.js"); var mvc = require('web.mvc'); -var SearchPanel = require('web.SearchPanel'); var viewUtils = require('web.viewUtils'); const { Component } = owl; @@ -60,7 +61,8 @@ var AbstractView = Factory.extend({ Model: AbstractModel, Renderer: AbstractRenderer, Controller: AbstractController, - SearchPanel: SearchPanel, + ControlPanel, + SearchPanel, }), /** @@ -116,10 +118,6 @@ var AbstractView = Factory.extend({ this.arch = this.fieldsView.arch; this.fields = this.fieldsView.viewFields; this.userContext = params.userContext || {}; - this.withControlPanel = this.withControlPanel && params.withControlPanel; - const searchPanelDisabled = 'search_panel' in params.context && !params.search_panel; - this.withSearchPanel = this.withSearchPanel && this.multi_record && - params.withSearchPanel && !searchPanelDisabled; // the boolean parameter 'isEmbedded' determines if the view should be // considered as a subview. For now this is only used by the graph @@ -147,8 +145,6 @@ var AbstractView = Factory.extend({ isEmbedded: isEmbedded, modelName: params.modelName, viewType: this.viewType, - withControlPanel: this.withControlPanel, - withSearchPanel: this.withSearchPanel, }; var controllerState = params.controllerState || {}; @@ -183,49 +179,32 @@ var AbstractView = Factory.extend({ this._updateMVCParams(params.searchQuery); } - if (this.withControlPanel) { - this.controlPanelModelConfig = { - env: Component.env, - actionId: action.id, - actionContext: Object.assign({}, this.loadParams.context || {}), - actionDomain: this.loadParams.domain || [], - modelName: params.modelName, - // control initialization - activateDefaultFavorite: params.activateDefaultFavorite, - dynamicFilters: params.dynamicFilters, - viewInfo: params.controlPanelFieldsView, - withSearchBar: params.withSearchBar, - // used to avoid timeRanges in query - searchMenuTypes: params.searchMenuTypes, - // avoid work to initialize - importedState: controllerState.cpState, - }; + this.withControlPanel = this.withControlPanel && params.withControlPanel; + this.withSearchPanel = this.withSearchPanel && + this.multi_record && params.withSearchPanel && + !('search_panel' in params.context && !params.search_panel); - const controlPanelModel = new ControlPanelModel(this.controlPanelModelConfig); - - const controlPanelProps = { - action, - breadcrumbs: params.breadcrumbs, - controlPanelModel, - fields: this.fields, - searchMenuTypes: params.searchMenuTypes, - view: this.fieldsView, - views: action.views && action.views.filter(v => v.multiRecord === this.multi_record), - withBreadcrumbs: params.withBreadcrumbs, - withSearchBar: params.withSearchBar, - }; - this.controllerParams.controlPanelModel = controlPanelModel; - this.controllerParams.controlPanelProps = controlPanelProps; + const searchModelParams = Object.assign({}, params, { action }); + if (this.withControlPanel || this.withSearchPanel) { + const { arch, fields, favoriteFilters } = params.controlPanelFieldsView || {}; + const archInfo = ActionModel.extractArchInfo({ search: arch }, this.viewType); + const controlPanelInfo = archInfo[this.config.ControlPanel.modelExtension]; + const searchPanelInfo = archInfo[this.config.SearchPanel.modelExtension]; + this.withSearchPanel = this.withSearchPanel && Boolean(searchPanelInfo); + Object.assign(searchModelParams, { + fields, + favoriteFilters, + controlPanelInfo, + searchPanelInfo, + }); } - - if (this.withSearchPanel) { - this.searchPanelParams = { - arch: (params.controlPanelFieldsView || {}).arch, - defaultNoFilter: params.searchPanelDefaultNoFilter, - fields: this.fields, - model: this.loadParams.modelName, - state: controllerState.spState, - }; + const searchModel = this._createSearchModel(searchModelParams); + this.controllerParams.searchModel = searchModel; + if (this.controllerParams.controlPanel) { + this.controllerParams.controlPanel.props.searchModel = searchModel; + } + if (this.controllerParams.searchPanel) { + this.controllerParams.searchPanel.props.searchModel = searchModel; } }, @@ -233,34 +212,107 @@ var AbstractView = Factory.extend({ // Public //-------------------------------------------------------------------------- + /** + * @param {Object} params + * @param {Object} extraExtensions + * @returns {ActionModel} + */ + _createSearchModel: function (params, extraExtensions) { + // Search model + common config + const { fields, favoriteFilters, controlPanelInfo, searchPanelInfo } = params; + const extensions = Object.assign({}, extraExtensions); + const importedState = params.controllerState || {}; + + // Control panel params + if (this.withControlPanel) { + // Control panel (Model) + const ControlPanelComponent = this.config.ControlPanel; + extensions[ControlPanelComponent.modelExtension] = { + actionId: params.action.id, + // control initialization + activateDefaultFavorite: params.activateDefaultFavorite, + archNodes: controlPanelInfo.children, + dynamicFilters: params.dynamicFilters, + favoriteFilters, + withSearchBar: params.withSearchBar, + }; + this.controllerParams.withControlPanel = true; + // Control panel (Component) + const controlPanelProps = { + action: params.action, + breadcrumbs: params.breadcrumbs, + fields: this.fields, + searchMenuTypes: params.searchMenuTypes, + view: this.fieldsView, + views: params.action.views && params.action.views.filter( + v => v.multiRecord === this.multi_record + ), + withBreadcrumbs: params.withBreadcrumbs, + withSearchBar: params.withSearchBar, + }; + this.controllerParams.controlPanel = { + Component: ControlPanelComponent, + props: controlPanelProps, + }; + } + + // Search panel params + if (this.withSearchPanel) { + // Search panel (Model) + const SearchPanelComponent = this.config.SearchPanel; + const defaultValues = {}; + Object.keys(this.loadParams.context).forEach((key) => { + let match = /^searchpanel_default_(.*)$/.exec(key); + if (match) { + defaultValues[match[1]] = this.loadParams.context[key]; + } + }); + extensions[SearchPanelComponent.modelExtension] = { + archNodes: searchPanelInfo.children, + defaultNoFilter: params.searchPanelDefaultNoFilter, + defaultValues, + }; + this.controllerParams.withSearchPanel = true; + this.rendererParams.withSearchPanel = true; + // Search panel (Component) + const searchPanelProps = { + importedState: importedState.searchPanel, + }; + if (searchPanelInfo.attrs.class) { + searchPanelProps.className = searchPanelInfo.attrs.class; + } + this.controllerParams.searchPanel = { + Component: SearchPanelComponent, + props: searchPanelProps, + }; + } + + const searchModel = new ActionModel(extensions, { + env: Component.env, + modelName: params.modelName, + context: Object.assign({}, this.loadParams.context), + domain: this.loadParams.domain || [], + importedState: importedState.searchModel, + searchMenuTypes: params.searchMenuTypes, + searchQuery: params.searchQuery, + fields, + }); + + return searchModel; + }, + /** * @override */ - getController: async function (parent) { + getController: async function () { const _super = this._super.bind(this); - if (this.withControlPanel) { - await this.controllerParams.controlPanelModel.isReady; - const query = this.controllerParams.controlPanelModel.getQuery(); - this._updateMVCParams(query); - } - let searchPanel = false; - if (this.withSearchPanel) { - const spProto = this.config.SearchPanel.prototype; - const { arch, fields } = this.searchPanelParams; - const searchPanelParams = spProto.computeSearchPanelParams(arch, fields, this.viewType); - if (searchPanelParams.sections) { - this.searchPanelParams.sections = searchPanelParams.sections; - this.rendererParams.withSearchPanel = true; - searchPanel = await this._createSearchPanel(parent, searchPanelParams); - } - } + await this.controllerParams.searchModel.load(); + const query = this.controllerParams.searchModel.get('query'); + this._updateMVCParams(query); // get the parent of the model if it already exists, as _super will // set the new controller as parent, which we don't want const modelParent = this.model && this.model.getParent(); const controller = await _super(...arguments); - if (searchPanel) { - searchPanel.setParent(controller); - } if (modelParent) { // if we already add a model, restore its parent this.model.setParent(modelParent); @@ -292,36 +344,6 @@ var AbstractView = Factory.extend({ // Private //-------------------------------------------------------------------------- - /** - * @private - * @param {Widget} parent - * @returns {Promise} resolved when the searchPanel is ready - */ - _createSearchPanel: async function (parent, params) { - var defaultValues = {}; - Object.keys(this.loadParams.context).forEach((key) => { - let match = /^searchpanel_default_(.*)$/.exec(key); - if (match) { - defaultValues[match[1]] = this.loadParams.context[key]; - } - }); - var controlPanelDomain = this.loadParams.domain; - const viewDomain = await this._getViewDomain(parent); - var spParams = _.extend({}, this.searchPanelParams, { - defaultValues: defaultValues, - searchDomain: controlPanelDomain, - viewDomain, - classes: params.classes || [], - }); - var searchPanel = new this.config.SearchPanel(parent, spParams); - this.controllerParams.searchPanel = searchPanel; - this.controllerParams.controlPanelDomain = controlPanelDomain; - await searchPanel.appendTo(document.createDocumentFragment()); - - var searchPanelDomain = searchPanel.getDomain(); - this.loadParams.domain = controlPanelDomain.concat(searchPanelDomain); - return searchPanel; - }, /** * @private * @param {Object} [action] @@ -363,17 +385,6 @@ var AbstractView = Factory.extend({ withSearchPanel: this.withSearchPanel, }; }, - /** - * Get the domain defined by the view. It is meant to be overridden. The parent - * is provided in case some subcomponents need to be retrieved. - * - * @private - * @param {Object} parent - * @returns {Promise} - */ - _getViewDomain: async function (parent) { - return []; - }, /** * Processes a fieldsView. In particular, parses its arch. * diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index f7028d93d08..e872fff811f 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -101,7 +101,7 @@ var KanbanController = BasicController.extend({ */ _addQuickCreate(groupId) { this._removeSampleData(async () => { - await this.update({ shouldUpdateControlPanel: false }, { reload: false }); + await this.update({ shouldUpdateSearchComponents: false }, { reload: false }); return this.renderer.addQuickCreate(groupId); }); }, diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index abe3856b91e..1e75a8dacc0 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -62,7 +62,7 @@ class="o_group_by_menu" fields="fields" /> -
-