summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/abstract_controller.js
diff options
context:
space:
mode:
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