From bb3bc7c9e4992dc07b10e87f87662bdd72f24d88 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Mon, 13 Jul 2020 13:23:36 +0000 Subject: [PATCH] [IMP] *: adapt modules to Owl search panel Adaptation of the rest of the environment to the changes regarding the refactored search panel and model. This commit includes the following changes: - Since the control panel model is now part of the search model, the view widget is responsible of its instantiation with the appropriate API; creating an ActionModel holding the required model extensions. - The controller is now importing/exporting the state of the entire search model as well as the current state of the search panel (given throught its props). Changes have been made to adapt to this behaviour. - The multiple components linked to the control panel model have been adapted to reflect the changes brought to it (they will now listen to the "searchModel" instead of the "controlPanelModel"). - The `test_utils_create` methods have also been adapted to properly instantiate a searchModel Part of task 2228968 Co-authored-by: Julien Mougenot Co-authored-by: Mathieu Duckerts-Antoine --- .../base_import/static/src/js/import_menu.js | 8 +- .../board/static/src/js/add_to_board_menu.js | 5 +- .../src/js/add_to_google_spreadsheet_menu.js | 2 +- .../static/src/widgets/discuss/discuss.js | 2 +- .../static/src/js/chrome/abstract_action.js | 61 +++-- .../js/chrome/action_manager_act_window.js | 3 +- .../src/js/control_panel/comparison_menu.js | 6 +- .../src/js/control_panel/control_panel.js | 16 +- .../js/control_panel/custom_favorite_item.js | 6 +- .../js/control_panel/custom_filter_item.js | 4 +- .../js/control_panel/custom_group_by_item.js | 4 +- .../src/js/control_panel/favorite_menu.js | 8 +- .../src/js/control_panel/filter_menu.js | 6 +- .../src/js/control_panel/groupby_menu.js | 6 +- .../static/src/js/control_panel/search_bar.js | 19 +- .../src/js/views/abstract_controller.js | 166 ++++++------- .../web/static/src/js/views/abstract_view.js | 233 +++++++++--------- .../src/js/views/kanban/kanban_controller.js | 2 +- addons/web/static/src/xml/base.xml | 8 +- addons/web/views/webclient_templates.xml | 2 +- 20 files changed, 288 insertions(+), 279 deletions(-) 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" /> -
-