[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 <jum@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
Julien Mougenot
2020-07-23 08:21:10 +00:00
committed by Mathieu Duckerts-Antoine
co-authored by Mathieu Duckerts-Antoine
parent 3162141e4d
commit bb3bc7c9e4
20 changed files with 288 additions and 279 deletions
@@ -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});
@@ -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 || {};
@@ -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);
@@ -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;
@@ -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);
}
},
@@ -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;
@@ -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');
}
/**
@@ -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);
@@ -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."),
@@ -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 = [];
@@ -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');
}
//---------------------------------------------------------------------
@@ -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;
}
}
@@ -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');
}
/**
@@ -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');
}
/**
@@ -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]);
}
@@ -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<Array[]>}
*/
_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.
+122 -111
View File
@@ -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<SearchPanel>} 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<Array[]>}
*/
_getViewDomain: async function (parent) {
return [];
},
/**
* Processes a fieldsView. In particular, parses its arch.
*
@@ -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);
});
},
+4 -4
View File
@@ -62,7 +62,7 @@
class="o_group_by_menu"
fields="fields"
/>
<ComparisonMenu t-if="props.searchMenuTypes.includes('comparison') and model.getFiltersOfType('comparison').length"
<ComparisonMenu t-if="props.searchMenuTypes.includes('comparison') and model.get('filters', f => f.type === 'comparison').length"
class="o_comparison_menu"
/>
<FavoriteMenu t-if="props.searchMenuTypes.includes('favorite')"
@@ -555,15 +555,15 @@
<t t-name="web.SearchBar" owl="1">
<div class="o_searchview_input_container">
<div t-foreach="model.getFacets()" t-as="facet" t-key="facet_index"
<div t-foreach="model.get('facets')" t-as="facet" t-key="facet_index"
tabindex="0"
class="o_searchview_facet"
role="img"
aria-label="search"
t-on-keydown="_onFacetKeydown(facet, facet_index)"
>
<span t-if="facet.title" class="o_searchview_facet_label" t-esc="facet.title"/>
<span t-else="" t-attf-class="o_searchview_facet_label {{ facet.icon }}"/>
<span t-if="facet.icon" t-attf-class="o_searchview_facet_label {{ facet.icon }}"/>
<span t-else="" class="o_searchview_facet_label" t-esc="facet.title"/>
<div class="o_facet_values">
<t t-foreach="facet.values" t-as="facetValue" t-key="facetValue_index">
<span t-if="!facetValue_first" class="o_facet_values_sep" t-esc="facet.separator"/>
+1 -1
View File
@@ -751,7 +751,7 @@
<script type="text/javascript" src="/web/static/tests/components/action_menus_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/components/dropdown_menu_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/control_panel/control_panel_model_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/control_panel/control_panel_model_extension_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/control_panel/control_panel_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/control_panel/comparison_menu_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/control_panel/favorite_menu_tests.js"></script>