summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/abstract_controller.js
diff options
context:
space:
mode:
authorJulien Mougenot <jum@odoo.com>2020-03-11 09:38:28 +0000
committerJulien Mougenot <jum@odoo.com>2020-03-13 14:08:33 +0000
commit18e5416636b19b0953b1e6e26bfaeab1ebecce4c (patch)
tree5be75af26f1707a5b8b2184545719f3628d2980f /addons/web/static/src/js/views/abstract_controller.js
parentfbf347498f1cc7b74ef373179b7bcae201715c24 (diff)
[REF] web: Adapt views/actions to new control panel
Some web actions/controllers/viewsl have been adapted to properly instantiate the newly refactored control panel and update it accordingly. We needed to adapt the starting mechanism of actions and controllers to ensure that the control panel instantiation did not delay the loading of any widget. Part of task 2196029 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/views/abstract_controller.js')
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js403
1 files changed, 175 insertions, 228 deletions
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index aa612cbea01..09451cdcbfa 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -15,86 +15,93 @@ odoo.define('web.AbstractController', function (require) {
var ActionMixin = require('web.ActionMixin');
var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
-var config = require('web.config');
-var core = require('web.core');
+const { ComponentWrapper } = require('web.OwlCompatibility');
+const ControlPanel = require('web.ControlPanel');
var mvc = require('web.mvc');
-var { WidgetAdapterMixin } = require('web.OwlCompatibility');
-
var session = require('web.session');
-var QWeb = core.qweb;
-var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin, {
+var AbstractController = mvc.Controller.extend(ActionMixin, {
custom_events: _.extend({}, ActionMixin.custom_events, {
navigation_move: '_onNavigationMove',
open_record: '_onOpenRecord',
- search: '_onSearch',
- switch_view: '_onSwitchView',
search_panel_domain_updated: '_onSearchPanelDomainUpdated',
+ switch_view: '_onSwitchView',
}),
events: {
'click a[type="action"]': '_onActionClicked',
},
/**
- * @override
- * @param {string} params.modelName
- * @param {string} [params.controllerID] an id to ease the communication
- * with upstream components
- * @param {ControlPanelController} [params.controlPanel]
- * @param {any} [params.handle] a handle that will be given to the model (some id)
- * @param {boolean} params.isMultiRecord
+ * @param {Object} param
* @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 {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 {string} [params.searchPanel]
* @param {string} params.viewType
+ * @param {boolean} [params.withControlPanel]
+ * @param {boolean} [params.withSearchPanel]
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
- this._controlPanel = params.controlPanel;
this._title = params.displayName;
this.modelName = params.modelName;
this.activeActions = params.activeActions;
this.controllerID = params.controllerID;
this.initialState = params.initialState;
this.bannerRoute = params.bannerRoute;
- this.isMultiRecord = params.isMultiRecord;
this.actionViews = params.actionViews;
this.viewType = params.viewType;
// use a DropPrevious to correctly handle concurrent updates
this.dp = new concurrency.DropPrevious();
+ this.withControlPanel = params.withControlPanel;
+ if (this.withControlPanel) {
+ this.controlPanelProps = params.controlPanelProps;
+ this._controlPanelModel = params.controlPanelModel;
+ }
+
+ this.withSearchPanel = params.withSearchPanel && params.searchPanel;
// the following attributes are used when there is a searchPanel
- this._searchPanel = params.searchPanel;
+ if (this.withSearchPanel) {
+ this._searchPanel = params.searchPanel;
+ }
this.controlPanelDomain = params.controlPanelDomain || [];
this.searchPanelDomain = this._searchPanel ? this._searchPanel.getDomain() : [];
},
+
/**
* Simply renders and updates the url.
*
* @returns {Promise}
*/
- start: function () {
- var self = this;
- if (this._searchPanel) {
+ start: async function () {
+ if (this.withSearchPanel) {
this.$('.o_content')
.addClass('o_controller_with_searchpanel')
.prepend(this._searchPanel.$el);
}
-
this.$el.addClass('o_view_controller');
- return this._super.apply(this, arguments).then(function () {
- var prom;
- if (self._controlPanel) {
- // render the ControlPanel elements (buttons, pager, sidebar...)
- prom = self._renderControlPanelElements().then(function (elements) {
- self.controlPanelElements = elements;
- self._controlPanel.$el.prependTo(self.$el);
- });
- }
- return Promise.resolve(prom);
- }).then(function () {
- return self._update(self.initialState);
- });
+ this.renderButtons();
+ const promises = [this._super(...arguments)];
+ if (this.withControlPanel) {
+ this._updateControlPanelProps(this.initialState);
+ this._controlPanelWrapper = new ComponentWrapper(this, ControlPanel, this.controlPanelProps);
+ this._controlPanelWrapper.env.bus.on('focus-view', this, () => this.renderer.giveFocus());
+ promises.push(this._controlPanelWrapper.mount(this.el, { position: 'first-child' }));
+ }
+ await Promise.all(promises);
+ await this._update(this.initialState, { shouldUpdateControlPanel: false });
+ this.updateButtons();
},
/**
* @override
@@ -103,34 +110,33 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
if (this.$buttons) {
this.$buttons.off();
}
- if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
- this.controlPanelElements.$switch_buttons.off();
- }
+ ActionMixin.destroy.call(this);
this._super.apply(this, arguments);
- WidgetAdapterMixin.destroy.call(this, ...arguments);
},
/**
* Called each time the controller is attached into the DOM.
*/
on_attach_callback: function () {
- if (this._controlPanel) {
- this._controlPanel.on_attach_callback();
- }
- if (this._searchPanel) {
+ ActionMixin.on_attach_callback.call(this);
+ if (this.withSearchPanel) {
this._searchPanel.on_attach_callback();
}
+ if (this.withControlPanel) {
+ this._controlPanelModel.on('search', this, this._onSearch);
+ this._controlPanelModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams);
+ }
this.renderer.on_attach_callback();
- WidgetAdapterMixin.on_attach_callback.call(this, ...arguments);
},
/**
* Called each time the controller is detached from the DOM.
*/
on_detach_callback: function () {
- if (this._controlPanel) {
- this._controlPanel.on_detach_callback();
+ ActionMixin.on_detach_callback.call(this);
+ if (this.withControlPanel) {
+ this._controlPanelModel.off('search', this);
+ this._controlPanelModel.off('get-controller-query-params', this);
}
this.renderer.on_detach_callback();
- WidgetAdapterMixin.on_detach_callback.call(this, ...arguments);
},
//--------------------------------------------------------------------------
@@ -174,10 +180,10 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
*/
exportState: function () {
var state = {};
- if (this._controlPanel) {
- state.cpState = this._controlPanel.exportState();
+ if (this.withControlPanel) {
+ state.cpState = this._controlPanelModel.exportState();
}
- if (this._searchPanel) {
+ if (this.withSearchPanel) {
state.spState = this._searchPanel.exportState();
}
return state;
@@ -203,21 +209,20 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
/**
* Short helper method to reload the view
*
- * @param {Object} [params] This object will simply be given to the update
+ * @param {Object} [params={}] This object will simply be given to the update
* @returns {Promise}
*/
- reload: async function (params) {
- params = params || {};
- var searchPanelUpdateProm;
- var controllerState = params.controllerState || {};
- var cpState = controllerState.cpState;
- if (this._controlPanel && cpState) {
- await this._controlPanel.importState(cpState).then(function (searchQuery) {
- params = _.extend({}, params, searchQuery);
- });
+ reload: async function (params = {}) {
+ let searchPanelUpdateProm;
+ 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);
}
- var postponeRendering = false;
- if (this._searchPanel) {
+ let postponeRendering = false;
+ if (this.withSearchPanel) {
this.controlPanelDomain = params.domain || this.controlPanelDomain;
if (controllerState.spState) {
this._searchPanel.importState(controllerState.spState);
@@ -235,28 +240,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
}
},
/**
- * For views that require a pager, this method will be called to allow the
- * controller to instantiate and render a pager. Note that in theory, the
- * controller can actually render whatever he wants in the pager zone. If
- * your view does not want a pager, just let this method empty.
- *
- * @param {jQuery Node} $node
- * @return {Promise}
- */
- renderPager: function ($node) {
- return Promise.resolve();
- },
- /**
- * Same as renderPager, but for the 'sidebar' zone (the zone with the menu
- * dropdown in the control panel next to the buttons)
- *
- * @param {jQuery Node} $node
- * @return {Promise}
- */
- renderSidebar: function ($node) {
- return Promise.resolve();
- },
- /**
* This is the main entry point for the controller. Changes from the search
* view arrive in this method, and internal changes can sometimes also call
* this method. It is basically the way everything notifies the controller
@@ -271,34 +254,21 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
*
* @returns {Promise}
*/
- update: function (params, options) {
- var self = this;
- var shouldReload = (options && 'reload' in options) ? options.reload : true;
- var def = shouldReload ? this.model.reload(this.handle, params) : Promise.resolve();
- // we check here that the updateIndex of the control panel hasn't changed
- // between the start of the update request and the moment the controller
- // asks the control panel to update itself ; indeed, it could happen that
- // another action/controller is executed during this one reloads itself,
- // and if that one finishes first, it replaces this controller in the DOM,
- // and this controller should no longer update the control panel.
- // note that this won't be necessary as soon as each controller will have
- // its own control panel
- var cpUpdateIndex = this._controlPanel && this._controlPanel.updateIndex;
- return this.dp.add(def).then(function (handle) {
- if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
- return;
- }
- self.handle = handle || self.handle; // update handle if we reloaded
- var state = self.model.get(self.handle);
- var localState = self.renderer.getLocalState();
- return self.dp.add(self.updateRendererState(state, params)).then(function () {
- if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
- return;
- }
- self.renderer.setLocalState(localState);
- return self._update(state, params);
- });
- });
+ update: async function (params, options = {}) {
+ const shouldReload = 'reload' in options ? options.reload : true;
+ if (shouldReload) {
+ this.handle = await this.dp.add(this.model.reload(this.handle, params));
+ }
+ const localState = this.renderer.getLocalState();
+ const state = this.model.get(this.handle);
+ const promises = [
+ this.updateRendererState(state, params).then(() => {
+ this.renderer.setLocalState(localState);
+ }),
+ this._update(state, params),
+ ];
+ await this.dp.add(Promise.all(promises));
+ this.updateButtons();
},
/**
* Update the state of the renderer (handle both Widget and Component
@@ -318,6 +288,16 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
// Private
//--------------------------------------------------------------------------
+
+ /**
+ * Meant to be overriden to return a proper object.
+ * @private
+ * @param {Object} [state]
+ * @return {(Object|null)}
+ */
+ _getPagingInfo: function (state) {
+ return null;
+ },
/**
* Return the current search domain. This is the searchDomain used to update
* the searchpanel. It returns the domain coming from the controlpanel. This
@@ -331,6 +311,15 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
return this.controlPanelDomain;
},
/**
+ * Meant to be overriden to return a proper object.
+ * @private
+ * @param {Object} [state]
+ * @return {(Object|null)}
+ */
+ _getActionMenuItems: function (state) {
+ return null;
+ },
+ /**
* This method is the way a view can notifies the outside world that
* something has changed. The main use for this is to update the url, for
* example with a new id.
@@ -360,107 +349,33 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
* @private
* @returns {Promise}
*/
- _renderBanner: function () {
+ _renderBanner: async function () {
if (this.bannerRoute !== undefined) {
- var self = this;
- return this.dp
- .add(this._rpc({route: this.bannerRoute}))
- .then(function (response) {
- if (!response.html) {
- self.$el.removeClass('o_has_banner');
- return Promise.resolve();
- }
- self.$el.addClass('o_has_banner');
- var $banner = $(response.html);
- // we should only display one banner at a time
- if (self._$banner && self._$banner.remove) {
- self._$banner.remove();
- }
- // Css and js are moved to <head>
- var defs = [];
- $('link[rel="stylesheet"]', $banner).each(function (i, link) {
- defs.push(ajax.loadCSS(link.href));
- link.remove();
- });
- $('script[type="text/javascript"]', $banner).each(function (i, js) {
- defs.push(ajax.loadJS(js.src));
- js.remove();
- });
- return Promise.all(defs).then(function () {
- $banner.insertBefore(self.$('> .o_content'));
- self._$banner = $banner;
- });
- });
- }
- return Promise.resolve();
- },
- /**
- * Renders the control elements (buttons, pager and sidebar) of the current
- * view.
- *
- * @private
- * @returns {Promise<Object>} resolved with an object containing the control
- * panel jQuery elements
- */
- _renderControlPanelElements: function () {
- var self = this;
- var elements = {
- $buttons: $('<div>'),
- $sidebar: $('<div>'),
- $pager: $('<div>'),
- };
-
- this.renderButtons(elements.$buttons);
- var sidebarProm = this.renderSidebar(elements.$sidebar);
- var pagerProm = this.renderPager(elements.$pager);
-
- return Promise.all([sidebarProm, pagerProm]).then(function () {
- // remove the unnecessary outer div
- elements = _.mapObject(elements, function ($node) {
- return $node && $node.contents();
+ const response = await this._rpc({route: this.bannerRoute});
+ if (!response.html) {
+ this.$el.removeClass('o_has_banner');
+ return Promise.resolve();
+ }
+ this.$el.addClass('o_has_banner');
+ var $banner = $(response.html);
+ // we should only display one banner at a time
+ if (this._$banner && this._$banner.remove) {
+ this._$banner.remove();
+ }
+ // Css and js are moved to <head>
+ var defs = [];
+ $('link[rel="stylesheet"]', $banner).each(function (i, link) {
+ defs.push(ajax.loadCSS(link.href));
+ link.remove();
});
- elements.$switch_buttons = self._renderSwitchButtons();
-
- return elements;
- });
- },
- /**
- * Renders the switch buttons and binds listeners on them.
- *
- * @private
- * @returns {jQuery}
- */
- _renderSwitchButtons: function () {
- var self = this;
- var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});
-
- if (views.length <= 1) {
- return $();
- }
-
- var template = config.device.isMobile ? 'ControlPanel.SwitchButtons.Mobile' : 'ControlPanel.SwitchButtons';
- var $switchButtons = $(QWeb.render(template, {
- viewType: this.viewType,
- views: views,
- }));
- // create bootstrap tooltips
- _.each(views, function (view) {
- $switchButtons.filter('.o_cp_switch_' + view.type).tooltip();
- });
- // add onclick event listener
- var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button');
- $switchButtonsFiltered.click(_.debounce(function (event) {
- var viewType = $(event.target).data('view-type');
- self.trigger_up('switch_view', {view_type: viewType});
- }, 200, true));
-
- // set active view's icon as view switcher button's icon in mobile
- if (config.device.isMobile) {
- var activeView = _.findWhere(views, {type: this.viewType});
- $switchButtons.find('.o_switch_view_button_icon').addClass('fa fa-lg ' + activeView.icon);
+ $('script[type="text/javascript"]', $banner).each(function (i, js) {
+ defs.push(ajax.loadJS(js.src));
+ js.remove();
+ });
+ await Promise.all(defs);
+ $banner.insertBefore(this.$('> .o_content'));
+ this._$banner = $banner;
}
-
- return $switchButtons;
},
/**
* @override
@@ -483,21 +398,57 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
*
* @private
* @param {Object} state the state given by the model
- * @param {Object} [params]
- * @param {Object[]} [params.breadcrumbs]
+ * @param {Object} [params={}]
+ * @param {Object} [params.shouldUpdateControlPanel]
* @returns {Promise}
*/
_update: function (state, params) {
// AAB: update the control panel -> this will be moved elsewhere at some point
- var cpContent = _.extend({}, this.controlPanelElements);
- this.updateControlPanel({
- breadcrumbs: params && params.breadcrumbs,
- cp_content: cpContent,
+ if (!this.$buttons) {
+ this.renderButtons();
+ }
+ const promises = [this._renderBanner()];
+ if (this.withControlPanel && params.shouldUpdateControlPanel !== false) {
+ this._updateControlPanelProps(state);
+ if (params.breadcrumbs) {
+ this.controlPanelProps.breadcrumbs = params.breadcrumbs;
+ }
+ promises.push(this.updateControlPanel());
+ }
+ this._pushState();
+ return Promise.all(promises);
+ },
+ /**
+ * Can be used to update the key 'cp_content'. This method is called in start and _update methods.
+ *
+ * @private
+ * @param {Object} state the state given by the model
+ */
+ _updateControlPanelProps(state) {
+ if (!this.controlPanelProps.cp_content) {
+ this.controlPanelProps.cp_content = {};
+ }
+ if (this.$buttons) {
+ this.controlPanelProps.cp_content.$buttons = this.$buttons;
+ }
+ Object.assign(this.controlPanelProps, {
+ actionMenus: this._getActionMenuItems(state),
+ pager: this._getPagingInfo(state),
title: this.getTitle(),
});
-
- this._pushState();
- return this._renderBanner();
+ },
+ /**
+ * @private
+ * @param {Object} state
+ * @param {Object} newProps
+ * @returns {Promise}
+ */
+ _updatePaging: async function (state, newProps) {
+ const pagingInfo = this._getPagingInfo(state);
+ if (pagingInfo) {
+ Object.assign(pagingInfo, newProps);
+ return this.updateControlPanel({ pager: pagingInfo });
+ }
},
//--------------------------------------------------------------------------
@@ -574,7 +525,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
switch (ev.data.direction) {
case 'up':
ev.stopPropagation();
- this._controlPanel.focusSearchBar();
+ this._controlPanelModel.trigger('focus-control-panel');
break;
case 'down':
ev.stopPropagation();
@@ -608,17 +559,13 @@ var AbstractController = mvc.Controller.extend(ActionMixin, WidgetAdapterMixin,
},
/**
* Called when there is a change in the search view, so the current action's
- * environment needs to be updated with the new domain, context and groupby.
+ * environment needs to be updated with the new domain, context, groupby,...
*
* @private
- * @param {OdooEvent} ev
- * @param {Array[]} ev.data.domain
- * @param {Object} ev.data.context
- * @param {string[]} ev.data.groupby
+ * @param {Object} searchQuery
*/
- _onSearch: function (ev) {
- ev.stopPropagation();
- this.reload(_.extend({offset: 0, groupsOffset: 0}, ev.data));
+ _onSearch: function (searchQuery) {
+ this.reload(_.extend({ offset: 0, groupsOffset: 0 }, searchQuery));
},
/**
* @private