summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views
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
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')
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js403
-rw-r--r--addons/web/static/src/js/views/abstract_view.js175
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js156
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js14
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js54
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js320
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js3
-rw-r--r--addons/web/static/src/js/views/form/form_view.js12
-rw-r--r--addons/web/static/src/js/views/graph/graph_controller.js268
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js112
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js79
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js83
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js2
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js7
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js221
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js121
-rw-r--r--addons/web/static/src/js/views/list/list_view.js6
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js67
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js87
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js6
-rw-r--r--addons/web/static/src/js/views/qweb/qweb_view.js13
-rw-r--r--addons/web/static/src/js/views/search_panel.js33
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js1
24 files changed, 1040 insertions, 1205 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
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 7dc3cc30563..c720a2a515e 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -26,11 +26,13 @@ odoo.define('web.AbstractView', function (require) {
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
-var ControlPanelView = require('web.ControlPanelView');
+const ControlPanelModel = require('web.ControlPanelModel');
var mvc = require('web.mvc');
var SearchPanel = require('web.SearchPanel');
var viewUtils = require('web.viewUtils');
+const { Component } = owl;
+
var Factory = mvc.Factory;
var AbstractView = Factory.extend({
@@ -83,7 +85,7 @@ var AbstractView = Factory.extend({
* @param {string} [params.displayName]
* @param {Array[]} [params.domain=[]]
* @param {Object[]} [params.dynamicFilters] transmitted to the
- * ControlPanelView
+ * ControlPanel
* @param {number[]} [params.ids]
* @param {boolean} [params.isEmbedded=false]
* @param {Object} [params.searchQuery={}]
@@ -143,9 +145,10 @@ var AbstractView = Factory.extend({
controllerID: params.controllerID,
displayName: params.displayName,
isEmbedded: isEmbedded,
- isMultiRecord: this.multi_record,
modelName: params.modelName,
viewType: this.viewType,
+ withControlPanel: this.withControlPanel,
+ withSearchPanel: this.withSearchPanel,
};
var controllerState = params.controllerState || {};
@@ -159,10 +162,6 @@ var AbstractView = Factory.extend({
res_id: currentId,
res_ids: controllerState.resIds || params.ids || (currentId ? [currentId] : undefined),
};
- // default_order is like:
- // 'name,id desc'
- // but we need it like:
- // [{name: 'id', asc: false}, {name: 'name', asc: true}]
var defaultOrder = this.arch.attrs.default_order;
if (defaultOrder) {
this.loadParams.orderedBy = _.map(defaultOrder.split(','), function (order) {
@@ -174,26 +173,50 @@ var AbstractView = Factory.extend({
this._updateMVCParams(params.searchQuery);
}
- this.controlPanelParams = {
- action: action,
- activateDefaultFavorite: params.activateDefaultFavorite,
- dynamicFilters: params.dynamicFilters,
- breadcrumbs: params.breadcrumbs,
- context: this.loadParams.context,
- domain: this.loadParams.domain,
- modelName: params.modelName,
- searchMenuTypes: params.searchMenuTypes,
- state: controllerState.cpState,
- viewInfo: params.controlPanelFieldsView,
- withBreadcrumbs: params.withBreadcrumbs,
- withSearchBar: params.withSearchBar,
- };
- this.searchPanelParams = {
- defaultNoFilter: params.searchPanelDefaultNoFilter,
- fields: this.fields,
- model: this.loadParams.modelName,
- state: controllerState.spState,
- };
+ 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,
+ };
+
+ 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;
+ }
+
+ if (this.withSearchPanel) {
+ this.searchPanelParams = {
+ arch: (params.controlPanelFieldsView || {}).arch,
+ defaultNoFilter: params.searchPanelDefaultNoFilter,
+ fields: this.fields,
+ model: this.loadParams.modelName,
+ state: controllerState.spState,
+ };
+ }
},
//--------------------------------------------------------------------------
@@ -203,41 +226,36 @@ var AbstractView = Factory.extend({
/**
* @override
*/
- getController: function (parent) {
- var self = this;
- var cpDef = this.withControlPanel && this._createControlPanel(parent);
- var spDef;
+ getController: async function (parent) {
+ 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) {
- var spProto = this.config.SearchPanel.prototype;
- var viewInfo = this.controlPanelParams.viewInfo;
- var searchPanelParams = spProto.computeSearchPanelParams(viewInfo, this.viewType);
+ 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;
- spDef = Promise.resolve(cpDef).then(this._createSearchPanel.bind(this, parent, searchPanelParams));
+ searchPanel = await this._createSearchPanel(parent, searchPanelParams);
}
}
-
- var _super = this._super.bind(this);
- return Promise.all([cpDef, spDef]).then(function ([controlPanel, searchPanel]) {
- // get the parent of the model if it already exists, as _super will
- // set the new controller as parent, which we don't want
- var modelParent = self.model && self.model.getParent();
- var prom = _super(parent);
- prom.then(function (controller) {
- if (controlPanel) {
- controlPanel.setParent(controller);
- }
- if (searchPanel) {
- searchPanel.setParent(controller);
- }
- if (modelParent) {
- // if we already add a model, restore its parent
- self.model.setParent(modelParent);
- }
- });
- return prom;
- });
+ // 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);
+ }
+ return controller;
},
/**
* Ensures that only one instance of AbstractModel is created
@@ -265,25 +283,6 @@ var AbstractView = Factory.extend({
//--------------------------------------------------------------------------
/**
- * Instantiates and starts a ControlPanelController.
- *
- * @private
- * @param {Widget} parent
- * @returns {Promise<ControlPanelController>} resolved when the controlPanel
- * is ready
- */
- _createControlPanel: function (parent) {
- var self = this;
- var controlPanelView = new ControlPanelView(this.controlPanelParams);
- return controlPanelView.getController(parent).then(function (controlPanel) {
- self.controllerParams.controlPanel = controlPanel;
- return controlPanel.appendTo(document.createDocumentFragment()).then(function () {
- self._updateMVCParams(controlPanel.getSearchQuery());
- return controlPanel;
- });
- });
- },
- /**
* @private
* @param {Widget} parent
* @returns {Promise<SearchPanel>} resolved when the searchPanel is ready
@@ -373,33 +372,23 @@ var AbstractView = Factory.extend({
* @private
* @param {Object} searchQuery
* @param {Object} searchQuery.context
- * @param {Object} [searchQuery.context.timeRangeMenuData={}]
- * @param {Array[]} [searchQuery.context.timeRangeMenuData.comparisonTimeRange=[]]
- * @param {string} [searchQuery.context.timeRangeMenuData.comparisonTimeRangeDescription='']
- * @param {string} [searchQuery.context.timeRangeMenuData.timeRangeDescription='']
- * @param {Array[]} [searchQuery.context.timeRangeMenuData.timeRange=[]]
+ * @param {Object} [searchQuery.timeRanges]
* @param {Array[]} searchQuery.domain
* @param {string[]} searchQuery.groupBy
*/
_updateMVCParams: function (searchQuery) {
- var timeRangeMenuData = searchQuery.context.timeRangeMenuData || {};
- var comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
- var comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription || '';
- var timeRangeDescription = timeRangeMenuData.timeRangeDescription || '';
this.loadParams = _.extend(this.loadParams, {
- compare: comparisonTimeRange.length > 0,
- comparisonField: timeRangeMenuData.comparisonField,
- comparisonTimeRange: comparisonTimeRange,
- comparisonTimeRangeDescription: comparisonTimeRangeDescription,
context: searchQuery.context,
domain: searchQuery.domain,
groupedBy: searchQuery.groupBy,
- timeRange: timeRangeMenuData.timeRange || [],
- timeRangeDescription: timeRangeMenuData.timeRangeDescription || '',
});
- this.loadParams.orderedBy = searchQuery.orderedBy ? searchQuery.orderedBy : this.loadParams.orderedBy;
- this.rendererParams.timeRangeDescription = timeRangeDescription;
- this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription;
+ this.loadParams.orderedBy = Array.isArray(searchQuery.orderedBy) && searchQuery.orderedBy.length ?
+ searchQuery.orderedBy :
+ this.loadParams.orderedBy;
+ if (searchQuery.timeRanges) {
+ this.loadParams.timeRanges = searchQuery.timeRanges;
+ this.rendererParams.timeRanges = searchQuery.timeRanges;
+ }
},
});
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 58e03efb94a..8b4672bc291 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -11,7 +11,6 @@ var AbstractController = require('web.AbstractController');
var core = require('web.core');
var Dialog = require('web.Dialog');
var FieldManagerMixin = require('web.FieldManagerMixin');
-var Pager = require('web.Pager');
var TranslationDialog = require('web.TranslationDialog');
var _t = core._t;
@@ -19,12 +18,12 @@ var _t = core._t;
var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
discard_changes: '_onDiscardChanges',
+ pager_changed: '_onPagerChanged',
reload: '_onReload',
resequence_records: '_onResequenceRecords',
set_dirty: '_onSetDirty',
load_optional_fields: '_onLoadOptionalFields',
save_optional_fields: '_onSaveOptionalFields',
- sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
}),
/**
@@ -52,12 +51,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @override
* @returns {Promise}
*/
- start: function () {
+ start: async function () {
// add classname to reflect the (absence of) access rights (used to
// correctly display the nocontent helper)
this.$el.toggleClass('o_cannot_create', !this.activeActions.create);
- return this._super.apply(this, arguments)
- .then(this._updateEnv.bind(this));
+ await this._super(...arguments);
},
//--------------------------------------------------------------------------
@@ -149,35 +147,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return this.model.isDirty(recordID || this.handle);
},
/**
- * @override
- */
- renderPager: function ($node, options) {
- var self = this;
- var params = this._getPagerParams();
- this.pager = new Pager(this, params.size, params.current_min, params.limit, options);
-
- this.pager.on('pager_changed', this, function (newState) {
- this.pager.disable();
- var data = this.model.get(this.handle, {raw: true});
- var limitChanged = (data.limit !== newState.limit);
- var reloadParams;
- if (data.groupedBy && data.groupedBy.length) {
- reloadParams = {groupsLimit: newState.limit, groupsOffset: newState.current_min - 1};
- } else {
- reloadParams = {limit: newState.limit, offset: newState.current_min - 1};
- }
- this.reload(reloadParams).then(function () {
- // reset the scroll position to the top on page changed only
- if (!limitChanged) {
- self.trigger_up('scrollTo', {top: 0});
- }
- }).then(this.pager.enable.bind(this.pager));
- });
- return this.pager.appendTo($node).then(function () {
- self._updatePager(); // to force proper visibility
- });
- },
- /**
* Saves the record whose ID is given if necessary (@see _saveRecord).
*
* @param {string} [recordID] - default to main recordID
@@ -218,13 +187,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @override
* @returns {Promise}
*/
- update: function (params, options) {
- var self = this;
+ update: async function (params, options) {
this.mode = params.mode || this.mode;
- return this._super(params, options).then(function () {
- self._updateEnv();
- self._updatePager();
- });
+ return this._super(params, options);
},
/**
* @override
@@ -486,45 +451,33 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return viewIdentifier;
},
/**
- * Return the params (current_min, limit and size) to pass to the pager,
+ * Return the params (currentMinimum, limit and size) to pass to the pager,
* according to the current state.
*
* @private
* @returns {Object}
*/
- _getPagerParams: function () {
- var state = this.model.get(this.handle, {raw: true});
- var isGrouped = state.groupedBy && state.groupedBy.length;
+ _getPagingInfo: function (state) {
+ const isGrouped = state.groupedBy && state.groupedBy.length;
return {
- current_min: (isGrouped ? state.groupsOffset : state.offset) + 1,
+ currentMinimum: (isGrouped ? state.groupsOffset : state.offset) + 1,
limit: isGrouped ? state.groupsLimit : state.limit,
size: isGrouped ? state.groupsCount : state.count,
};
},
/**
- * Returns the new sidebar env
+ * Return the new actionMenus props.
*
+ * @override
* @private
- * @return {Object} the new sidebar env
*/
- _getSidebarEnv: function () {
+ _getActionMenuItems: function (state) {
return {
- context: this.model.get(this.handle).getContext(),
activeIds: this.getSelectedIds(),
- model: this.modelName,
+ context: state.getContext(),
};
},
/**
- * Determine whether or not the pager must be displayed (probably depending
- * on the current state). Controllers must override this to implement their
- * own logic.
- *
- * @private
- */
- _isPagerVisible: function () {
- return true;
- },
- /**
* Sort function used to sort the fields by names, to compute the optional fields keys
*
* @param {Object} left
@@ -633,24 +586,16 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* and notifies the component chain (by bubbling an event up)
*
* @private
- */
- _updateEnv: function () {
- var env = this.model.get(this.handle, {env: true});
- if (this.sidebar) {
- var sidebarEnv = this._getSidebarEnv();
- this.sidebar.updateEnv(sidebarEnv);
- }
- },
- /**
- * Update the pager with the current state.
- *
- * @private
- */
- _updatePager: function () {
- if (this.pager) {
- this.pager.updateState(this._getPagerParams());
- this.pager.do_toggle(this._isPagerVisible());
- }
+ * @param {Object} [newProps={}]
+ */
+ _updateControlPanel: function (newProps = {}) {
+ const state = this.model.get(this.handle);
+ const props = Object.assign(newProps, {
+ actionMenus: this._getActionMenuItems(state),
+ pager: this._getPagingInfo(state),
+ title: this.getTitle(),
+ });
+ return this.updateControlPanel(props);
},
//--------------------------------------------------------------------------
@@ -694,6 +639,27 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin._onFieldChanged.apply(this, arguments);
},
/**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onPagerChanged: async function (ev) {
+ ev.stopPropagation();
+ const { currentMinimum, limit } = ev.data;
+ const state = this.model.get(this.handle, { raw: true });
+ const reloadParams = state.groupedBy && state.groupedBy.length ? {
+ groupsLimit: limit,
+ groupsOffset: currentMinimum - 1,
+ } : {
+ limit,
+ offset: currentMinimum - 1,
+ };
+ await this.reload(reloadParams);
+ // reset the scroll position to the top on page changed only
+ if (state.limit === limit) {
+ this.trigger_up('scrollTo', { top: 0 });
+ }
+ },
+ /**
* When a reload event triggers up, we need to reload the full view.
* For example, after a form view dialog saved some data.
*
@@ -735,24 +701,19 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {string} ev.data.handleField
*/
_onResequenceRecords: function (ev) {
- var self = this;
-
this.trigger_up('mutexify', {
- action: function () {
- var state = self.model.get(self.handle);
- var resIDs = _.map(ev.data.recordIds, function (recordID) {
- return _.findWhere(state.data, {id: recordID}).res_id;
- });
- var options = {
+ action: async () => {
+ let state = this.model.get(this.handle);
+ const resIDs = ev.data.recordIds
+ .map(recordID => state.data.find(d => d.id === recordID).res_id);
+ const options = {
offset: ev.data.offset,
field: ev.data.handleField,
};
- return self.model.resequence(self.modelName, resIDs, self.handle, options)
- .then(function () {
- self._updateEnv();
- state = self.model.get(self.handle);
- return self.renderer.updateState(state, {noRender: true});
- });
+ await this.model.resequence(this.modelName, resIDs, this.handle, options);
+ this._updateControlPanel();
+ state = this.model.get(this.handle);
+ return this.renderer.updateState(state, { noRender: true });
},
});
},
@@ -798,17 +759,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
this.model.setDirty(ev.data.dataPointID);
},
/**
- * Handler used to get all the data necessary when a custom action is
- * performed through the sidebar.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSidebarDataAsked: function (ev) {
- var sidebarEnv = this._getSidebarEnv();
- ev.data.callback(sidebarEnv);
- },
- /**
* open the translation view for the current field
*
* @private
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 9ba7d39b8dc..a69f6878023 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -4798,7 +4798,7 @@ var BasicModel = AbstractModel.extend({
element.context = options.context;
}
if (options.orderedBy !== undefined) {
- element.orderedBy = options.orderedBy || element.orderedBy;
+ element.orderedBy = (options.orderedBy.length && options.orderedBy) || element.orderedBy;
}
if (options.domain !== undefined) {
element.domain = options.domain;
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index ba407b766c1..c0d3e415321 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -49,12 +49,26 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, {
* Called each time the renderer is attached into the DOM.
*/
on_attach_callback: function () {
+ for (const handle in this.allFieldWidgets) {
+ this.allFieldWidgets[handle].forEach(widget => {
+ if (widget.on_attach_callback) {
+ widget.on_attach_callback();
+ }
+ });
+ }
WidgetAdapterMixin.on_attach_callback.call(this);
},
/**
* Called each time the renderer is detached from the DOM.
*/
on_detach_callback: function () {
+ for (const handle in this.allFieldWidgets) {
+ this.allFieldWidgets[handle].forEach(widget => {
+ if (widget.on_detach_callback) {
+ widget.on_detach_callback();
+ }
+ });
+ }
WidgetAdapterMixin.on_detach_callback.call(this);
},
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index d59ea076188..dddfc122c00 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -37,6 +37,9 @@ var CalendarController = AbstractController.extend({
updateRecord: '_onUpdateRecord',
viewUpdated: '_onViewUpdated',
}),
+ events: _.extend({}, AbstractController.prototype.events, {
+ today_button_click: '_onTodayButtonClicked'
+ }),
/**
* @override
* @param {Widget} parent
@@ -60,34 +63,16 @@ var CalendarController = AbstractController.extend({
// The quickCreating attribute ensures that we don't do several create
this.quickCreating = false;
},
- /**
- * Overrides to unbind handler on the control panel mobile 'Today' button.
- *
- * @override
- */
- destroy: function () {
- this._super.apply(this, arguments);
- if (this.$todayButton) {
- this.$todayButton.off();
- }
- },
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
- * @override
- * @returns {string}
- */
- getTitle: function () {
- return this._title;
- },
- /**
* Render the buttons according to the CalendarView.buttons template and
* add listeners on it. Set this.$buttons with the produced jQuery element
*
- * @param {jQueryElement} [$node] a jQuery node where the rendered buttons
+ * @param {jQuery} [$node] a jQuery node where the rendered buttons
* should be inserted. $node may be undefined, in which case the Calendar
* inserts them into this.options.$buttons or into a div of its template
*/
@@ -120,25 +105,6 @@ var CalendarController = AbstractController.extend({
this.$('.o_calendar_buttons').replaceWith(this.$buttons);
}
},
- /**
- * In mobile, we want to display a special 'Today' button on the bottom
- * right corner of the control panel. This is the pager area, and as there
- * is no pager in Calendar views, we fool the system by defining a fake
- * pager (which is actually our button) such that it will be inserted in the
- * desired place.
- *
- * @todo get rid of this hack once the ControlPanel layout will be reworked
- *
- * @param {jQueryElement} $node the button should be appended to this
- * element to be displayed in the bottom right corner of the control panel
- */
- renderPager: function ($node) {
- if (config.device.isMobile) {
- this.$todayButton = $(QWeb.render('CalendarView.TodayButtonMobile'));
- this.$todayButton.on('click', this._move.bind(this, 'today'));
- $node.append(this.$todayButton);
- }
- },
//--------------------------------------------------------------------------
// Private
@@ -424,7 +390,14 @@ var CalendarController = AbstractController.extend({
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
self.quickCreating = false;
- })
+ });
+ },
+ /**
+ * In mobile, we display a special 'Today' button on the bottom right corner
+ * of the control panel. Its click events are handled here.
+ */
+ _onTodayButtonClicked: function () {
+ this._move('today');
},
/**
* @private
@@ -446,7 +419,8 @@ var CalendarController = AbstractController.extend({
this.$buttons.find('.active').removeClass('active');
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
}
- this._setTitle(this.displayName + ' (' + event.data.title + ')');
+ const title = `${this.displayName} (${event.data.title})`;
+ return this.updateControlPanel({ title });
},
});
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 9d361693595..5c54afb7e41 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -5,7 +5,6 @@ var BasicController = require('web.BasicController');
var core = require('web.core');
var Dialog = require('web.Dialog');
var dialogs = require('web.view_dialogs');
-var Sidebar = require('web.Sidebar');
var _t = core._t;
var qweb = core.qweb;
@@ -24,7 +23,7 @@ var FormController = BasicController.extend({
/**
* @override
*
- * @param {boolean} params.hasSidebar
+ * @param {boolean} params.hasActionMenus
* @param {Object} params.toolbarActions
*/
init: function (parent, model, renderer, params) {
@@ -34,7 +33,7 @@ var FormController = BasicController.extend({
this.disableAutofocus = params.disableAutofocus;
this.footerToButtons = params.footerToButtons;
this.defaultButtons = params.defaultButtons;
- this.hasSidebar = params.hasSidebar;
+ this.hasActionMenus = params.hasActionMenus;
this.toolbarActions = params.toolbarActions || {};
},
/**
@@ -88,10 +87,9 @@ var FormController = BasicController.extend({
* for the new record.
* @returns {Promise}
*/
- createRecord: function (parentID) {
- var self = this;
- var record = this.model.get(this.handle, {raw: true});
- return this.model.load({
+ createRecord: async function (parentID) {
+ const record = this.model.get(this.handle, { raw: true });
+ const handle = await this.model.load({
context: record.getContext(),
fields: record.fields,
fieldsInfo: record.fieldsInfo,
@@ -100,15 +98,14 @@ var FormController = BasicController.extend({
res_ids: record.res_ids,
type: 'record',
viewType: 'form',
- }).then(function (handle) {
- self.handle = handle;
- self._updateEnv();
- return self._setMode('edit');
});
+ this.handle = handle;
+ this._updateControlPanel();
+ return this._setMode('edit');
},
/**
* Returns the current res_id, wrapped in a list. This is only used by the
- * sidebar (and the debugmanager)
+ * action menus (and the debugmanager)
*
* @override
*
@@ -142,35 +139,34 @@ var FormController = BasicController.extend({
* have to use them instead of the standard buttons.
*
* @override method from AbstractController
- * @param {jQueryElement} $node
+ * @param {jQuery} [$node]
*/
renderButtons: function ($node) {
- var $footer = this.footerToButtons ? this.renderer.$('footer') : null;
+ var $footer = this.footerToButtons ? this.renderer.$el && this.renderer.$('footer') : null;
var mustRenderFooterButtons = $footer && $footer.length;
- if (!this.defaultButtons && !mustRenderFooterButtons) {
- return;
+ if ((this.defaultButtons && !this.$buttons) || mustRenderFooterButtons) {
+ this.$buttons = $('<div/>');
+ if (mustRenderFooterButtons) {
+ this.$buttons.append($footer);
+ } else {
+ this.$buttons.append(qweb.render("FormView.buttons", {widget: this}));
+ this.$buttons.on('click', '.o_form_button_edit', this._onEdit.bind(this));
+ this.$buttons.on('click', '.o_form_button_create', this._onCreate.bind(this));
+ this.$buttons.on('click', '.o_form_button_save', this._onSave.bind(this));
+ this.$buttons.on('click', '.o_form_button_cancel', this._onDiscard.bind(this));
+ this._assignSaveCancelKeyboardBehavior(this.$buttons.find('.o_form_buttons_edit'));
+ this.$buttons.find('.o_form_buttons_edit').tooltip({
+ delay: {show: 200, hide:0},
+ title: function(){
+ return qweb.render('SaveCancelButton.tooltip');
+ },
+ trigger: 'manual',
+ });
+ }
}
- this.$buttons = $('<div/>');
- if (mustRenderFooterButtons) {
- this.$buttons.append($footer);
-
- } else {
- this.$buttons.append(qweb.render("FormView.buttons", {widget: this}));
- this.$buttons.on('click', '.o_form_button_edit', this._onEdit.bind(this));
- this.$buttons.on('click', '.o_form_button_create', this._onCreate.bind(this));
- this.$buttons.on('click', '.o_form_button_save', this._onSave.bind(this));
- this.$buttons.on('click', '.o_form_button_cancel', this._onDiscard.bind(this));
- this._assignSaveCancelKeyboardBehavior(this.$buttons.find('.o_form_buttons_edit'));
- this.$buttons.find('.o_form_buttons_edit').tooltip({
- delay: {show: 200, hide:0},
- title: function(){
- return qweb.render('SaveCancelButton.tooltip');
- },
- trigger: 'manual',
- });
- this._updateButtons();
+ if (this.$buttons && $node) {
+ this.$buttons.appendTo($node);
}
- this.$buttons.appendTo($node);
},
/**
* The form view has to prevent a click on the pager if the form is dirty
@@ -180,71 +176,58 @@ var FormController = BasicController.extend({
* @param {Object} options
* @returns {Promise}
*/
- renderPager: function ($node, options) {
- options = _.extend({}, options, {
+ _getPagingInfo: function () {
+ // Only display the pager if we are not on a new record.
+ if (this.model.isNew(this.handle)) {
+ return null;
+ }
+ return Object.assign(this._super(...arguments), {
validate: this.canBeDiscarded.bind(this),
});
- return this._super($node, options);
},
/**
- * Instantiate and render the sidebar if a sidebar is requested
- * Sets this.sidebar
- * @param {jQuery} [$node] a jQuery node where the sidebar should be
- * inserted
- * @return {Promise}
+ * @override
+ * @private
**/
- renderSidebar: function ($node) {
- var self = this;
- if (this.hasSidebar) {
- var otherItems = [];
- const activeField = this.model.getActiveField(this.initialState);
- const activeFieldValue = this.initialState.data[activeField];
- if (this.archiveEnabled && activeField) {
- var classname = "o_sidebar_item_archive" + (activeFieldValue ? "" : " o_hidden")
- otherItems.push({
- label: _t("Archive"),
- callback: function () {
- Dialog.confirm(self, _t("Are you sure that you want to archive this record?"), {
- confirm_callback: self._toggleArchiveState.bind(self, true),
+ _getActionMenuItems: function () {
+ if (!this.hasActionMenus || this.mode === 'edit') {
+ return null;
+ }
+ const props = this._super(...arguments);
+ const activeField = this.model.getActiveField(this.initialState);
+ const otherActionItems = [];
+ if (this.archiveEnabled && activeField) {
+ if (this.initialState.data[activeField]) {
+ otherActionItems.push({
+ description: _t("Archive"),
+ callback: () => {
+ Dialog.confirm(this, _t("Are you sure that you want to archive this record?"), {
+ confirm_callback: () => this._toggleArchiveState(true),
});
},
- classname: classname,
- });
- classname = "o_sidebar_item_unarchive" + (activeFieldValue ? " o_hidden" : "")
- otherItems.push({
- label: _t("Unarchive"),
- callback: this._toggleArchiveState.bind(this, false),
- classname: classname,
- });
- }
- if (this.is_action_enabled('delete')) {
- otherItems.push({
- label: _t('Delete'),
- callback: this._onDeleteRecord.bind(this),
});
- }
- if (this.is_action_enabled('create') && this.is_action_enabled('duplicate')) {
- otherItems.push({
- label: _t('Duplicate'),
- callback: this._onDuplicateRecord.bind(this),
+ } else {
+ otherActionItems.push({
+ description: _t("Unarchive"),
+ callback: () => this._toggleArchiveState(false),
});
}
- this.sidebar = new Sidebar(this, {
- editable: this.is_action_enabled('edit'),
- viewType: 'form',
- env: {
- context: this.model.get(this.handle).getContext(),
- activeIds: this.getSelectedIds(),
- model: this.modelName,
- },
- actions: _.extend(this.toolbarActions, {other: otherItems}),
+ }
+ if (this.activeActions.create && this.activeActions.duplicate) {
+ otherActionItems.push({
+ description: _t("Duplicate"),
+ callback: () => this._onDuplicateRecord(this),
});
- return this.sidebar.appendTo($node).then(function() {
- // Show or hide the sidebar according to the view mode
- self._updateSidebar();
+ }
+ if (this.activeActions.delete) {
+ otherActionItems.push({
+ description: _t("Delete"),
+ callback: () => this._onDeleteRecord(this),
});
}
- return Promise.resolve();
+ return Object.assign(props, {
+ items: Object.assign(this.toolbarActions, { other: otherActionItems }),
+ });
},
/**
* Show a warning message if the user modified a translated field. For each
@@ -252,32 +235,29 @@ var FormController = BasicController.extend({
*
* @override
*/
- saveRecord: function () {
- var self = this;
- return this._super.apply(this, arguments).then(function (changedFields) {
- // the title could have been changed
- self._setTitle(self.getTitle());
- self._updateEnv();
+ saveRecord: async function () {
+ const changedFields = await this._super(...arguments);
+ // the title could have been changed
+ this._updateControlPanel();
- if (_t.database.multi_lang && changedFields.length) {
- // need to make sure changed fields that should be translated
- // are displayed with an alert
- var fields = self.renderer.state.fields;
- var data = self.renderer.state.data;
- var alertFields = {};
- for (var k = 0; k < changedFields.length; k++) {
- var field = fields[changedFields[k]];
- var fieldData = data[changedFields[k]];
- if (field.translate && fieldData) {
- alertFields[changedFields[k]] = field;
- }
- }
- if (!_.isEmpty(alertFields)) {
- self.renderer.updateAlertFields(alertFields);
+ if (_t.database.multi_lang && changedFields.length) {
+ // need to make sure changed fields that should be translated
+ // are displayed with an alert
+ var fields = this.renderer.state.fields;
+ var data = this.renderer.state.data;
+ var alertFields = {};
+ for (var k = 0; k < changedFields.length; k++) {
+ var field = fields[changedFields[k]];
+ var fieldData = data[changedFields[k]];
+ if (field.translate && fieldData) {
+ alertFields[changedFields[k]] = field;
}
}
- return changedFields;
- });
+ if (!_.isEmpty(alertFields)) {
+ this.renderer.updateAlertFields(alertFields);
+ }
+ }
+ return changedFields;
},
/**
* Overrides to force the viewType to 'form', so that we ensure that the
@@ -285,12 +265,32 @@ var FormController = BasicController.extend({
*
* @override
*/
- update: function (params, options) {
+ update: async function (params, options) {
if ('currentId' in params && !params.currentId) {
this.mode = 'edit'; // if there is no record, we are in 'edit' mode
}
params = _.extend({viewType: 'form', mode: this.mode}, params);
- return this._super(params, options);
+ await this._super(params, options);
+ this.autofocus();
+ },
+ /**
+ * @override
+ */
+ updateButtons: function () {
+ if (!this.$buttons) {
+ return;
+ }
+ if (this.footerToButtons) {
+ var $footer = this.renderer.$el && this.renderer.$('footer');
+ if ($footer && $footer.length) {
+ this.$buttons.empty().append($footer);
+ }
+ }
+ var edit_mode = (this.mode === 'edit');
+ this.$buttons.find('.o_form_buttons_edit')
+ .toggleClass('o_hidden', !edit_mode);
+ this.$buttons.find('.o_form_buttons_view')
+ .toggleClass('o_hidden', edit_mode);
},
//--------------------------------------------------------------------------
@@ -413,15 +413,6 @@ var FormController = BasicController.extend({
this.renderer.enableButtons();
},
/**
- * Only display the pager if we are not on a new record.
- *
- * @override
- * @private
- */
- _isPagerVisible: function () {
- return !this.model.isNew(this.handle);
- },
- /**
* Hook method, called when record(s) has been deleted.
*
* @override
@@ -464,74 +455,6 @@ var FormController = BasicController.extend({
}
return this._super.apply(this, arguments);
},
- /**
- * Updates the controller's title according to the new state
- *
- * @override
- * @private
- * @param {Object} state
- * @returns {Promise}
- */
- _update: function () {
- var self = this;
-
- return this._super.apply(this, arguments).then(function() {
- var title = self.getTitle();
- self._setTitle(title);
- self._updateButtons();
- self._updateSidebar();
- self.autofocus();
- });
- },
- /**
- * @private
- */
- _updateButtons: function () {
- if (this.$buttons) {
- if (this.footerToButtons) {
- var $footer = this.renderer.$('footer');
- if ($footer.length) {
- this.$buttons.empty().append($footer);
- }
- }
- var edit_mode = (this.mode === 'edit');
- this.$buttons.find('.o_form_buttons_edit')
- .toggleClass('o_hidden', !edit_mode);
- this.$buttons.find('.o_form_buttons_view')
- .toggleClass('o_hidden', edit_mode);
- }
- },
- /**
- * Show or hide the sidebar according to the actual_mode
- * @private
- */
- _updateSidebar: function () {
- if (this.sidebar) {
- this.sidebar.do_toggle(this.mode === 'readonly');
- // Hide/Show Archive/Unarchive dropdown items
- // We could have toggled the o_hidden class on the
- // item theirselves, but the items are redrawed
- // at each update, based on the initial definition
- var archive_item = _.find(this.sidebar.items.other, function(item) {
- return item.classname && item.classname.includes('o_sidebar_item_archive')
- })
- var unarchive_item = _.find(this.sidebar.items.other, function(item) {
- return item.classname && item.classname.includes('o_sidebar_item_unarchive')
- })
- const state = this.model.get(this.handle, {raw: true});
- const activeField = this.model.getActiveField(state);
- const activeFieldValue = this.renderer.state.data[activeField];
- if (archive_item && unarchive_item) {
- if (activeFieldValue) {
- archive_item.classname = 'o_sidebar_item_archive';
- unarchive_item.classname = 'o_sidebar_item_unarchive o_hidden';
- } else {
- archive_item.classname = 'o_sidebar_item_archive o_hidden';
- unarchive_item.classname = 'o_sidebar_item_unarchive';
- }
- }
- }
- },
//--------------------------------------------------------------------------
// Handlers
@@ -623,18 +546,15 @@ var FormController = BasicController.extend({
this._discardChanges();
},
/**
- * Called when the user clicks on 'Duplicate Record' in the sidebar
+ * Called when the user clicks on 'Duplicate Record' in the action menus
*
* @private
*/
- _onDuplicateRecord: function () {
- var self = this;
- this.model.duplicateRecord(this.handle)
- .then(function (handle) {
- self.handle = handle;
- self._updateEnv();
- self._setMode('edit');
- });
+ _onDuplicateRecord: async function () {
+ const handle = await this.model.duplicateRecord(this.handle);
+ this.handle = handle;
+ this._updateControlPanel();
+ this._setMode('edit');
},
/**
* Called when the user wants to edit the current record -> @see _setMode
@@ -777,7 +697,7 @@ var FormController = BasicController.extend({
});
},
/**
- * Called when clicking on 'Archive' or 'Unarchive' in the sidebar.
+ * Called when clicking on 'Archive' or 'Unarchive' in the action menus.
*
* @private
* @param {boolean} archive
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index f3c663891b0..1af48aabf73 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -49,9 +49,6 @@ var FormRenderer = BasicRenderer.extend({
*/
on_attach_callback: function () {
this._isInDom = true;
- _.forEach(this.allFieldWidgets, function (widgets){
- _.invoke(widgets, 'on_attach_callback');
- });
this._super.apply(this, arguments);
},
/**
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 13d73cf37c2..9d13fd36b16 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -25,19 +25,19 @@ var FormView = BasicView.extend({
* @override
*/
init: function (viewInfo, params) {
- var hasSidebar = params.hasSidebar;
+ var hasActionMenus = params.hasActionMenus;
this._super.apply(this, arguments);
var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
this.loadParams.type = 'record';
// this is kind of strange, but the param object is modified by
- // AbstractView, so we only need to use its hasSidebar value if it was
+ // AbstractView, so we only need to use its hasActionMenus value if it was
// not already present in the beginning of this method
- if (hasSidebar === undefined) {
- hasSidebar = params.hasSidebar;
+ if (hasActionMenus === undefined) {
+ hasActionMenus = params.hasActionMenus;
}
- this.controllerParams.hasSidebar = hasSidebar;
+ this.controllerParams.hasActionMenus = hasActionMenus;
this.controllerParams.disableAutofocus = params.disable_autofocus;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.footerToButtons = params.footerToButtons;
@@ -75,7 +75,7 @@ var FormView = BasicView.extend({
params.withControlPanel = !(inDialog || inline);
params.footerToButtons = inDialog;
params.hasSearchView = inDialog ? false : params.hasSearchView;
- params.hasSidebar = !inDialog && !inline;
+ params.hasActionMenus = !inDialog && !inline;
params.searchMenuTypes = inDialog ? [] : params.searchMenuTypes;
if (inDialog || inline || fullscreen) {
params.mode = 'edit';
diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js
index 1c580a25bd6..f47bfaeb5bb 100644
--- a/addons/web/static/src/js/views/graph/graph_controller.js
+++ b/addons/web/static/src/js/views/graph/graph_controller.js
@@ -1,16 +1,30 @@
odoo.define('web.GraphController', function (require) {
"use strict";
+
/*---------------------------------------------------------
* Odoo Graph view
*---------------------------------------------------------*/
-var core = require('web.core');
-var AbstractController = require('web.AbstractController');
-var GroupByMenuMixin = require('web.GroupByMenuMixin');
+const AbstractController = require('web.AbstractController');
+const { ComponentWrapper } = require('web.OwlCompatibility');
+const DropdownMenu = require('web.DropdownMenu');
+const { DEFAULT_INTERVAL, INTERVAL_OPTIONS } = require('web.searchUtils');
+const { qweb } = require('web.core');
+
+class CarretDropdownMenu extends DropdownMenu {
+ /**
+ * @override
+ */
+ get displayCaret() {
+ return true;
+ }
+}
-var qweb = core.qweb;
+var GraphController = AbstractController.extend({
+ custom_events: _.extend({}, AbstractController.prototype.custom_events, {
+ item_selected: '_onItemSelected',
+ }),
-var GraphController = AbstractController.extend(GroupByMenuMixin,{
/**
* @override
* @param {Widget} parent
@@ -22,12 +36,12 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
* @param {string[]} params.groupableFields,
*/
init: function (parent, model, renderer, params) {
- GroupByMenuMixin.init.call(this);
this._super.apply(this, arguments);
this.measures = params.measures;
// this parameter condition the appearance of a 'Group By'
// button in the control panel owned by the graph view.
this.isEmbedded = params.isEmbedded;
+ this.withButtons = params.withButtons;
// this parameter determines what is the list of fields
// that may be used within the groupby menu available when
@@ -76,6 +90,18 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
};
},
/**
+ * @override
+ */
+ reload: async function () {
+ const promises = [this._super(...arguments)];
+ if (this.withButtons) {
+ const state = this.model.get();
+ this.measures.forEach(m => m.isActive = m.fieldName === state.measure);
+ promises.push(this.measureMenu.update({ items: this.measures }));
+ }
+ return Promise.all(promises);
+ },
+ /**
* Render the buttons according to the GraphView.buttons and
* add listeners on it.
* Set this.$buttons with the produced jQuery element
@@ -85,86 +111,62 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
* nothing
*/
renderButtons: function ($node) {
- if ($node) {
- var context = {
- measures: _.sortBy(_.pairs(_.omit(this.measures, '__count__')), function (x) { return x[1].string.toLowerCase(); }),
- };
- this.$buttons = $(qweb.render('GraphView.buttons', context));
- this.$measureList = this.$buttons.find('.o_graph_measures_list');
- this.$buttons.find('button').tooltip();
- this.$buttons.click(this._onButtonClick.bind(this));
- this._updateButtons();
- this.$buttons.appendTo($node);
- if (this.isEmbedded) {
- this._addGroupByMenu($node, this.groupableFields).then(function(){
- var groupByButton = $node.find('.o_dropdown_toggler_btn');
- groupByButton.removeClass("o_dropdown_toggler_btn btn btn-secondary dropdown-toggle");
- groupByButton.addClass("btn dropdown-toggle btn-outline-secondary");
- });
- }
+ this.$buttons = $(qweb.render('GraphView.buttons'));
+ this.$buttons.find('button').tooltip();
+ this.$buttons.click(ev => this._onButtonClick(ev));
+ if (this.withButtons) {
+ const actionsContainer = this.$buttons[0];
+ const promises = [];
+ const state = this.model.get();
+ const fragment = document.createDocumentFragment();
+ // Instantiate and append MeasureMenu
+ this.measures.forEach(m => m.isActive = m.fieldName === state.measure);
+ this.measureMenu = new ComponentWrapper(this, CarretDropdownMenu, {
+ title: "Measures",
+ items: this.measures,
+ });
+ promises.push(this.measureMenu.mount(fragment).then(() => {
+ actionsContainer.appendChild(this.measureMenu.el);
+ this.measureMenu.el.classList.add('o_graph_measures_list');
+ }));
+ if ($node) {
+ if (this.isEmbedded) {
+ // Instantiate and append GroupBy menu
+ this.groupByMenu = new ComponentWrapper(this, CarretDropdownMenu, {
+ title: "Group By",
+ icon: 'fa fa-bars',
+ items: this._getGroupBys(state.groupBy),
+ });
+ promises.push(this.groupByMenu.mount(fragment).then(() => {
+ actionsContainer.appendChild(this.groupByMenu.el);
+ this.groupByMenu.el.classList.add('o_group_by_menu');
+ }));
+ }
+ this.$buttons.appendTo($node);
+ }
+ Promise.all(promises).then(() => {
+ // Similar behaviour for all buttons
+ const buttons = actionsContainer.querySelectorAll('.o_dropdown_toggler_btn');
+ for (const button of buttons) {
+ button.classList.remove('o_dropdown_toggler_btn', 'btn-secondary');
+ if (this.isEmbedded) {
+ button.classList.add('btn-outline-secondary');
+ } else {
+ button.classList.add('btn-primary');
+ button.tabIndex = 0;
+ }
+ }
+ });
}
},
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /*
- * override
- *
- * @private
- * @param {string[]} groupBy
- */
- _setGroupby: function (groupBy) {
- this.update({groupBy: groupBy});
- },
- /**
- * @todo remove this and directly calls update. Update should be overridden
- * and modified to call _updateButtons
- *
- * @private
- *
- * @param {'pie'|'line'|'bar'} mode
- */
- _setMode: function (mode) {
- this.update({mode: mode});
- this._updateButtons();
- },
/**
- * @todo same as _setMode
+ * Makes sure that the buttons in the control panel matches the current
+ * state (so, correct active buttons and stuff like that).
*
- * @param {string} measure should be a valid (and aggregatable) field name
- */
- _setMeasure: function (measure) {
- var self = this;
- this.update({measure: measure}).then(function () {
- self._updateButtons();
- });
- },
- /**
- * @private
- *
- * @param {boolean} stacked
- */
- _toggleStackMode: function (stacked) {
- this.update({stacked: stacked});
- this._updateButtons();
- },
- /**
- * override
- *
- * @private
- */
- _update: function () {
- this._updateButtons();
- return this._super.apply(this, arguments);
- },
- /**
- * makes sure that the buttons in the control panel matches the current
- * state (so, correct active buttons and stuff like that)
+ * @override
*/
- _updateButtons: function () {
+ updateButtons: function () {
if (!this.$buttons) {
return;
}
@@ -178,9 +180,60 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
.data('stacked', state.stacked)
.toggleClass('active', state.stacked)
.toggleClass('o_hidden', state.mode !== 'bar');
- _.each(this.$measureList.find('.dropdown-item'), function (item) {
- var $item = $(item);
- $item.toggleClass('selected', $item.data('field') === state.measure);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the items used by the Group By menu in embedded mode.
+ *
+ * @private
+ * @param {string[]} activeGroupBys
+ * @returns {Object[]}
+ */
+ _getGroupBys(activeGroupBys) {
+ const normalizedGroupBys = this._normalizeActiveGroupBys(activeGroupBys);
+ const groupBys = Object.keys(this.groupableFields).map(fieldName => {
+ const field = this.groupableFields[fieldName];
+ const groupByActivity = normalizedGroupBys.filter(gb => gb.fieldName === fieldName);
+ const groupBy = {
+ id: fieldName,
+ isActive: Boolean(groupByActivity.length),
+ description: field.string,
+ itemType: 'groupBy',
+ };
+ if (['date', 'datetime'].includes(field.type)) {
+ groupBy.hasOptions = true;
+ const activeOptionIds = groupByActivity.map(gb => gb.interval);
+ groupBy.options = Object.values(INTERVAL_OPTIONS).map(o => {
+ return Object.assign({}, o, { isActive: activeOptionIds.includes(o.id) });
+ });
+ }
+ return groupBy;
+ }).sort((gb1, gb2) => {
+ return gb1.description.localeCompare(gb2.description);
+ });
+ return groupBys;
+ },
+
+ /**
+ * This method puts the active groupBys in a convenient form.
+ *
+ * @private
+ * @param {string[]} activeGroupBys
+ * @returns {Object[]} normalizedGroupBys
+ */
+ _normalizeActiveGroupBys(activeGroupBys) {
+ return activeGroupBys.map(groupBy => {
+ const fieldName = groupBy.split(':')[0];
+ const field = this.groupableFields[fieldName];
+ const normalizedGroupBy = { fieldName };
+ if (['date', 'datetime'].includes(field.type)) {
+ normalizedGroupBy.interval = groupBy.split(':')[1] || DEFAULT_INTERVAL;
+ }
+ return normalizedGroupBy;
});
},
@@ -196,18 +249,51 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
*/
_onButtonClick: function (ev) {
var $target = $(ev.target);
- var field;
if ($target.hasClass('o_graph_button')) {
if (_.contains(['bar','line', 'pie'], $target.data('mode'))) {
- this._setMode($target.data('mode'));
+ this.update({ mode: $target.data('mode') });
} else if ($target.data('mode') === 'stack') {
- this._toggleStackMode(!$target.data('stacked'));
+ this.update({ stacked: !$target.data('stacked') });
+ }
+ }
+ },
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onItemSelected(ev) {
+ const item = ev.data.item;
+ if (this.isEmbedded && item.itemType === 'groupBy') {
+ const fieldName = item.id;
+ const optionId = ev.data.option && ev.data.option.id;
+ const activeGroupBys = this.model.get().groupBy;
+ if (optionId) {
+ const normalizedGroupBys = this._normalizeActiveGroupBys(activeGroupBys);
+ const index = normalizedGroupBys.findIndex(ngb =>
+ ngb.fieldName === fieldName && ngb.interval === optionId);
+ if (index === -1) {
+ activeGroupBys.push(fieldName + ':' + optionId);
+ } else {
+ activeGroupBys.splice(index, 1);
+ }
+ } else {
+ const groupByFieldNames = activeGroupBys.map(gb => gb.split(':')[0]);
+ const indexOfGroupby = groupByFieldNames.indexOf(fieldName);
+ if (indexOfGroupby === -1) {
+ activeGroupBys.push(fieldName);
+ } else {
+ activeGroupBys.splice(indexOfGroupby, 1);
+ }
}
- } else if ($target.parents('.o_graph_measures_list').length) {
- ev.preventDefault();
- ev.stopPropagation();
- field = $target.data('field');
- this._setMeasure(field);
+ this.update({ groupBy: activeGroupBys });
+ this.groupByMenu.update({
+ items: this._getGroupBys(activeGroupBys),
+ });
+ } else if (item.itemType === 'measure') {
+ this.update({ measure: item.fieldName });
+ this.measures.forEach(m => m.isActive = m.fieldName === item.fieldName);
+ this.measureMenu.update({ items: this.measures });
}
},
});
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index d8b8df2f1fd..8dcb241ae9c 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -2,7 +2,7 @@ odoo.define('web.GraphModel', function (require) {
"use strict";
var core = require('web.core');
-const { DEFAULT_INTERVAL, rank } = require('web.controlPanelViewParameters');
+const { DEFAULT_INTERVAL, rankInterval } = require('web.searchUtils');
var _t = core._t;
@@ -35,10 +35,7 @@ return AbstractModel.extend({
* @returns {Object}
*/
get: function () {
- var self = this;
- return _.extend({}, this.chart, {
- comparisonFieldIndex: self._getComparisonFieldIndex(),
- });
+ return _.extend({}, this.chart);
},
/**
* Initial loading.
@@ -47,21 +44,16 @@ return AbstractModel.extend({
* should be done by the graphView I think.
*
* @param {Object} params
- * @param {boolean} params.compare
* @param {Object} params.context
* @param {Object} params.fields
- * @param {string[]} params.comparisonTimeRange
* @param {string[]} params.domain
* @param {string[]} params.groupBys a list of valid field names
* @param {string[]} params.groupedBy a list of valid field names
* @param {boolean} params.stacked
- * @param {string[]} params.timeRange
- * @param {string} params.comparisonField
- * @param {string} params.comparisonTimeRangeDescription
* @param {string} params.measure a valid field name
* @param {'pie'|'bar'|'line'} params.mode
* @param {string} params.modelName
- * @param {string} params.timeRangeDescription
+ * @param {Object} params.timeRanges
* @returns {Promise} The promise does not return a handle, we don't need
* to keep track of various entities.
*/
@@ -70,11 +62,7 @@ return AbstractModel.extend({
this.initialGroupBys = groupBys;
this.fields = params.fields;
this.modelName = params.modelName;
- this.chart = {
- comparisonField: params.comparisonField,
- comparisonTimeRange: params.comparisonTimeRange,
- comparisonTimeRangeDescription: params.comparisonTimeRangeDescription,
- compare: params.compare,
+ this.chart = Object.assign({
context: params.context,
dataPoints: [],
domain: params.domain,
@@ -83,11 +71,12 @@ return AbstractModel.extend({
mode: params.context.graph_mode || params.mode,
origins: [],
stacked: params.stacked,
- timeRange: params.timeRange,
- timeRangeDescription: params.timeRangeDescription,
- };
- this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy);
- return this._loadGraph(this._getDomains());
+ timeRanges: params.timeRanges,
+ });
+
+ this._computeDerivedParams();
+
+ return this._loadGraph();
},
/**
* Reload data. It is similar to the load function. Note that we ignore the
@@ -104,6 +93,7 @@ return AbstractModel.extend({
* @param {string[]} [params.groupBy]
* @param {string} [params.measure] a valid field name
* @param {string} [params.mode] one of 'bar', 'pie', 'line'
+ * @param {Object} [params.timeRanges]
* @returns {Promise}
*/
reload: function (handle, params) {
@@ -112,22 +102,6 @@ return AbstractModel.extend({
this.chart.groupBy = params.context.graph_groupbys || this.chart.groupBy;
this.chart.measure = params.context.graph_measure || this.chart.measure;
this.chart.mode = params.context.graph_mode || this.chart.mode;
- var timeRangeMenuData = params.context.timeRangeMenuData;
- if (timeRangeMenuData) {
- this.chart.comparisonField = timeRangeMenuData.comparisonField || undefined;
- this.chart.comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
- this.chart.compare = this.chart.comparisonTimeRange.length > 0;
- this.chart.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription;
- this.chart.timeRange = timeRangeMenuData.timeRange || [];
- this.chart.timeRangeDescription = timeRangeMenuData.timeRangeDescription;
- } else {
- this.chart.comparisonField = undefined;
- this.chart.comparisonTimeRange = [];
- this.chart.compare = false;
- this.chart.comparisonTimeRangeDescription = undefined;
- this.chart.timeRange = [];
- this.chart.timeRangeDescription = undefined;
- }
}
if ('domain' in params) {
this.chart.domain = params.domain;
@@ -138,7 +112,12 @@ return AbstractModel.extend({
if ('measure' in params) {
this.chart.measure = params.measure;
}
- this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy);
+ if ('timeRanges' in params) {
+ this.chart.timeRanges = params.timeRanges;
+ }
+
+ this._computeDerivedParams();
+
if ('mode' in params) {
this.chart.mode = params.mode;
return Promise.resolve();
@@ -147,7 +126,7 @@ return AbstractModel.extend({
this.chart.stacked = params.stacked;
return Promise.resolve();
}
- return this._loadGraph(this._getDomains());
+ return this._loadGraph();
},
//--------------------------------------------------------------------------
@@ -155,28 +134,34 @@ return AbstractModel.extend({
//--------------------------------------------------------------------------
/**
+ * Compute this.chart.processedGroupBy, this.chart.domains, this.chart.origins,
+ * and this.chart.comparisonFieldIndex.
+ * Those parameters are determined by this.chart.timeRanges, this.chart.groupBy, and this.chart.domain.
+ *
* @private
- * @returns {number}
- */
- _getComparisonFieldIndex: function () {
- var groupBys = this.chart.processedGroupBy.map(function (gb) {
- return gb.split(":")[0];
- });
- return groupBys.indexOf(this.chart.comparisonField);
- },
- /**
- * @private
- * @returns {Array[]}
*/
- _getDomains: function () {
- var domains = [this.chart.domain.concat(this.chart.timeRange)];
- this.chart.origins = [this.chart.timeRangeDescription || ""];
- if (this.chart.compare) {
- domains.push(this.chart.domain.concat(this.chart.comparisonTimeRange));
- this.chart.origins.push(this.chart.comparisonTimeRangeDescription);
+ _computeDerivedParams: function () {
+ this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy);
+
+ const { range, rangeDescription, comparisonRange, comparisonRangeDescription, fieldName } = this.chart.timeRanges;
+ if (range) {
+ this.chart.domains = [this.chart.domain.concat(range)];
+ this.chart.origins = [rangeDescription];
+ if (comparisonRange) {
+ this.chart.domains.push(this.chart.domain.concat(comparisonRange));
+ this.chart.origins.push(comparisonRangeDescription);
+ }
+ const groupBys = this.chart.processedGroupBy.map(function (gb) {
+ return gb.split(":")[0];
+ });
+ this.chart.comparisonFieldIndex = groupBys.indexOf(fieldName);
+ } else {
+ this.chart.domains = [this.chart.domain];
+ this.chart.origins = [""];
+ this.chart.comparisonFieldIndex = -1;
}
- return domains;
},
+
/**
* Fetch and process graph data. It is basically a(some) read_group(s)
* with correct fields for each domain. We have to do some light processing
@@ -184,10 +169,9 @@ return AbstractModel.extend({
* with an aggregation function, such as my_date:week.
*
* @private
- * @param {Array[]} domains
* @returns {Promise}
*/
- _loadGraph: function (domains) {
+ _loadGraph: function () {
var self = this;
this.chart.dataPoints = [];
var groupBy = this.chart.processedGroupBy;
@@ -205,10 +189,10 @@ return AbstractModel.extend({
}
var context = _.extend({fill_temporal: true}, this.chart.context);
- var defs = [];
- domains.forEach(function (domain, originIndex) {
- defs.push(self._rpc({
+ var proms = [];
+ this.chart.domains.forEach(function (domain, originIndex) {
+ proms.push(self._rpc({
model: self.modelName,
method: 'read_group',
context: context,
@@ -218,7 +202,7 @@ return AbstractModel.extend({
lazy: false,
}).then(self._processData.bind(self, originIndex)));
});
- return Promise.all(defs);
+ return Promise.all(proms);
},
/**
* Since read_group is insane and returns its result on different keys
@@ -286,7 +270,7 @@ return AbstractModel.extend({
}
if (groupBysMap.has(fieldName)) {
const registeredInterval = groupBysMap.get(fieldName);
- if (rank(registeredInterval) < rank(interval)) {
+ if (rankInterval(registeredInterval) < rankInterval(interval)) {
groupBysMap.set(fieldName, interval);
}
} else {
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index 64c65694784..488424536e6 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -16,8 +16,8 @@ var GraphRenderer = require('web.GraphRenderer');
var _t = core._t;
var _lt = core._lt;
-var controlPanelViewParameters = require('web.controlPanelViewParameters');
-var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
+var searchUtils = require('web.searchUtils');
+var GROUPABLE_TYPES = searchUtils.GROUPABLE_TYPES;
var GraphView = AbstractView.extend({
display_name: _lt('Graph'),
@@ -39,54 +39,81 @@ var GraphView = AbstractView.extend({
init: function (viewInfo, params) {
this._super.apply(this, arguments);
- var self = this;
- var measure;
- var groupBys = [];
- var measures = {__count__: {string: _t("Count"), type: "integer"}};
- var groupableFields = {};
- this.fields.__count__ = {string: _t("Count"), type: "integer"};
+ const additionalMeasures = params.additionalMeasures || [];
+ let measure;
+ const measures = {};
+ const measureStrings = {};
+ const groupBys = [];
+ const groupableFields = {};
+ this.fields.__count__ = { string: _t("Count"), type: 'integer' };
- var measureString = {};
-
- this.arch.children.forEach(function (field) {
- var fieldName = field.attrs.name;
+ this.arch.children.forEach(field => {
+ let fieldName = field.attrs.name;
if (fieldName === "id") {
return;
}
- var interval = field.attrs.interval;
+ const interval = field.attrs.interval;
if (interval) {
fieldName = fieldName + ':' + interval;
}
if (field.attrs.type === 'measure') {
+ const { string } = this.fields[fieldName];
measure = fieldName;
- measures[fieldName] = self.fields[fieldName];
+ measures[fieldName] = {
+ description: string,
+ fieldName,
+ groupNumber: 0,
+ isActive: false,
+ itemType: 'measure',
+ };
} else {
groupBys.push(fieldName);
}
if (field.attrs.string) {
- measureString[fieldName] = field.attrs.string;
+ measureStrings[fieldName] = field.attrs.string;
}
});
- _.each(this.fields, function (field, name) {
+ for (const name in this.fields) {
+ const field = this.fields[name];
if (name !== 'id' && field.store === true) {
- if (_.contains(['integer', 'float', 'monetary'], field.type) ||
- _.contains(params.additionalMeasures, name)) {
- measures[name] = field;
+ if (
+ ['integer', 'float', 'monetary'].includes(field.type) ||
+ additionalMeasures.includes(name)
+ ) {
+ measures[name] = {
+ description: field.string,
+ fieldName: name,
+ groupNumber: 0,
+ isActive: false,
+ itemType: 'measure',
+ };
}
- if (_.contains(GROUPABLE_TYPES, field.type)) {
+ if (GROUPABLE_TYPES.includes(field.type)) {
groupableFields[name] = field;
}
}
- });
-
- _.each(measureString, function (string, name) {
+ }
+ for (const name in measureStrings) {
if (measures[name]) {
- measures[name].string = string;
+ measures[name].description = measureStrings[name];
}
- });
+ }
- this.controllerParams.measures = measures;
+ const sortedMeasures = Object.values(measures).sort((a, b) => {
+ const descA = a.description.toLowerCase();
+ const descB = b.description.toLowerCase();
+ return descA > descB ? 1 : descA < descB ? -1 : 0;
+ });
+ const countMeasure = {
+ description: _t("Count"),
+ fieldName: '__count__',
+ groupNumber: 1,
+ isActive: false,
+ itemType: 'measure',
+ };
+ this.controllerParams.withButtons = params.withButtons !== false;
+ this.controllerParams.measures = [...sortedMeasures, countMeasure];
this.controllerParams.groupableFields = groupableFields;
this.rendererParams.fields = this.fields;
this.rendererParams.title = this.arch.attrs.title; // TODO: use attrs.string instead
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 95012eeebfc..cca0a43cb5e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -58,21 +58,35 @@ var KanbanController = BasicController.extend({
//--------------------------------------------------------------------------
/**
- * @param {jQueryElement} $node
- * @returns {Promise}
+ * @param {jQuery} [$node]
*/
renderButtons: function ($node) {
- if (this.hasButtons && this.is_action_enabled('create')) {
- this.$buttons = $(qweb.render(this.buttons_template, {
- btnClass: 'btn-primary',
- widget: this,
- }));
- this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
- this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this));
- this._updateButtons();
- return Promise.resolve(this.$buttons.appendTo($node));
+ if (!this.hasButtons || !this.is_action_enabled('create')) {
+ return;
+ }
+ this.$buttons = $(qweb.render(this.buttons_template, {
+ btnClass: 'btn-primary',
+ widget: this,
+ }));
+ this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
+ this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this));
+ if ($node) {
+ this.$buttons.appendTo($node);
}
- return Promise.resolve();
+ },
+ /**
+ * In grouped mode, set 'Create' button as btn-secondary if there is no column
+ * (except if we can't create new columns)
+ *
+ * @override
+ */
+ updateButtons: function () {
+ if (!this.$buttons) {
+ return;
+ }
+ var state = this.model.get(this.handle, {raw: true});
+ var createHidden = this.is_action_enabled('group_create') && state.isGroupedByM2ONoColumn;
+ this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden);
},
//--------------------------------------------------------------------------
@@ -100,9 +114,11 @@ var KanbanController = BasicController.extend({
* @override
* @private
*/
- _isPagerVisible: function () {
- var state = this.model.get(this.handle, {raw: true});
- return !!(state.count && !state.groupedBy.length);
+ _getPagingInfo: function (state) {
+ if (!(state.count && !state.groupedBy.length)) {
+ return null;
+ }
+ return this._super(...arguments);
},
/**
* @private
@@ -185,33 +201,7 @@ var KanbanController = BasicController.extend({
*/
_resequenceRecords: function (column_id, ids) {
var self = this;
- return this.model.resequence(this.modelName, ids, column_id).then(function () {
- self._updateEnv();
- });
- },
- /**
- * Overrides to update the control panel buttons when the state is updated.
- *
- * @override
- * @private
- */
- _update: function () {
- this._updateButtons();
- return this._super.apply(this, arguments);
- },
- /**
- * In grouped mode, set 'Create' button as btn-secondary if there is no column
- * (except if we can't create new columns)
- *
- * @private
- * @override from abstract controller
- */
- _updateButtons: function () {
- if (this.$buttons) {
- var state = this.model.get(this.handle, {raw: true});
- var createHidden = this.is_action_enabled('group_create') && state.isGroupedByM2ONoColumn;
- this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden);
- }
+ return this.model.resequence(this.modelName, ids, column_id);
},
//--------------------------------------------------------------------------
@@ -236,7 +226,6 @@ var KanbanController = BasicController.extend({
}).then(function () {
return self.update({}, {reload: false});
}).then(function () {
- self._updateButtons();
self.renderer.quickCreateToggleFold();
self.renderer.trigger_up("quick_create_column_created");
});
@@ -389,7 +378,6 @@ var KanbanController = BasicController.extend({
this.model.loadMore(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
- self._updateEnv();
});
},
/**
@@ -412,7 +400,6 @@ var KanbanController = BasicController.extend({
// function that updates the kanban view once the record has been added
// it receives the local id of the created record in arguments
var update = function (db_id) {
- self._updateEnv();
var columnState = self.model.getColumn(db_id);
var state = self.model.get(self.handle);
@@ -459,9 +446,7 @@ var KanbanController = BasicController.extend({
*/
_onResequenceColumn: function (ev) {
var self = this;
- this._resequenceColumns(ev.data.ids).then(function () {
- self._updateEnv();
- });
+ this._resequenceColumns(ev.data.ids);
},
/**
* @private
@@ -481,7 +466,6 @@ var KanbanController = BasicController.extend({
return self.renderer.updateColumn(db_id, data, options);
})
.then(function () {
- self._updateEnv();
if (ev.data.onSuccess) {
ev.data.onSuccess();
}
@@ -520,7 +504,6 @@ var KanbanController = BasicController.extend({
prom.then(function (dbID) {
var data = self.model.get(dbID);
self.renderer.updateColumn(dbID, data);
- self._updateEnv();
});
}
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 2f2def8d850..3025adde2cb 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -179,7 +179,7 @@ var KanbanModel = BasicModel.extend({
* @override
*/
load: function (params) {
- this.defaultGroupedBy = params.groupBy;
+ this.defaultGroupedBy = params.groupBy || [];
params.groupedBy = (params.groupedBy && params.groupedBy.length) ? params.groupedBy : this.defaultGroupedBy;
return this._super(params);
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 7159dda9618..d49b4a73c4e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -108,11 +108,8 @@ var KanbanView = BasicView.extend({
*/
_updateMVCParams: function () {
this._super.apply(this, arguments);
- if (!this.noDefaultGroupby) {
- var defaultGroupBy = this.arch.attrs.default_group_by;
- this.loadParams.groupBy = defaultGroupBy ?
- [defaultGroupBy] :
- (this.loadParams.groupedBy || []);
+ if (!this.noDefaultGroupby && this.arch.attrs.default_group_by) {
+ this.loadParams.groupBy = [this.arch.attrs.default_group_by];
}
},
});
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index d6b42ee1043..41dfa18c8bc 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -12,7 +12,6 @@ var BasicController = require('web.BasicController');
var DataExport = require('web.DataExport');
var Dialog = require('web.Dialog');
var ListConfirmDialog = require('web.ListConfirmDialog');
-var Sidebar = require('web.Sidebar');
var _t = core._t;
var qweb = core.qweb;
@@ -42,19 +41,20 @@ var ListController = BasicController.extend({
* @override
* @param {Object} params
* @param {boolean} params.editable
- * @param {boolean} params.hasSidebar
+ * @param {boolean} params.hasActionMenus
* @param {Object} params.toolbarActions
* @param {boolean} params.noLeaf
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
- this.hasSidebar = params.hasSidebar;
+ this.hasActionMenus = params.hasActionMenus;
this.toolbarActions = params.toolbarActions || {};
this.editable = params.editable;
this.noLeaf = params.noLeaf;
this.selectedRecords = params.selectedRecords || [];
this.multipleRecordsSavingPromise = null;
this.fieldChangedPrevented = false;
+ this.selectAll = false;
Object.defineProperty(this, 'mode', {
get: () => this.renderer.isEditable() ? 'edit' : 'readonly',
set: () => {},
@@ -78,8 +78,8 @@ var ListController = BasicController.extend({
*/
getActiveDomain: function () {
var self = this;
- if (this.$('thead .o_list_record_selector input').prop('checked')) {
- var searchQuery = this._controlPanel ? this._controlPanel.getSearchQuery() : {};
+ if (this.selectAll) {
+ var searchQuery = this._controlPanelModel ? this._controlPanelModel.getQuery() : {};
var record = self.model.get(self.handle, {raw: true});
return record.getDomain().concat(searchQuery.domain || []);
}
@@ -125,74 +125,26 @@ var ListController = BasicController.extend({
* induce the leaving of the current row.
*
* @override
- * @param {jQuery} $node
*/
renderButtons: function ($node) {
- if (!this.noLeaf && this.hasButtons) {
- this.$buttons = $(qweb.render(this.buttons_template, {widget: this}));
- this.$buttons.on('click', '.o_list_button_add', this._onCreateRecord.bind(this));
-
- this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add'));
- this.$buttons.find('.o_list_button_add').tooltip({
- delay: {show: 200, hide: 0},
- title: function () {
- return qweb.render('CreateButton.tooltip');
- },
- trigger: 'manual',
- });
- this.$buttons.on('mousedown', '.o_list_button_discard', this._onDiscardMousedown.bind(this));
- this.$buttons.on('click', '.o_list_button_discard', this._onDiscard.bind(this));
- this.$buttons.appendTo($node);
+ if (this.noLeaf || !this.hasButtons) {
+ return;
}
- },
- /**
- * Render the sidebar (the 'action' menu in the control panel, right of the
- * main buttons)
- *
- * @param {jQuery Node} $node
- * @returns {Promise}
- */
- renderSidebar: function ($node) {
- var self = this;
- if (this.hasSidebar) {
- var other = [{
- label: _t("Export"),
- callback: this._onExportData.bind(this)
- }];
- if (this.archiveEnabled) {
- other.push({
- label: _t("Archive"),
- callback: function () {
- Dialog.confirm(self, _t("Are you sure that you want to archive all the selected records?"), {
- confirm_callback: self._toggleArchiveState.bind(self, true),
- });
- }
- });
- other.push({
- label: _t("Unarchive"),
- callback: this._toggleArchiveState.bind(this, false)
- });
- }
- if (this.is_action_enabled('delete')) {
- other.push({
- label: _t('Delete'),
- callback: this._onDeleteSelectedRecords.bind(this)
- });
- }
- this.sidebar = new Sidebar(this, {
- editable: this.is_action_enabled('edit'),
- env: {
- context: this.model.get(this.handle, {raw: true}).getContext(),
- activeIds: this.getSelectedIds(),
- model: this.modelName,
- },
- actions: _.extend(this.toolbarActions, {other: other}),
- });
- return this.sidebar.appendTo($node).then(function() {
- self._toggleSidebar();
- });
+ this.$buttons = $(qweb.render(this.buttons_template, {widget: this}));
+ this.$buttons.on('click', '.o_list_button_add', this._onCreateRecord.bind(this));
+ this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add'));
+ this.$buttons.find('.o_list_button_add').tooltip({
+ delay: {show: 200, hide: 0},
+ title: function () {
+ return qweb.render('CreateButton.tooltip');
+ },
+ trigger: 'manual',
+ });
+ this.$buttons.on('mousedown', '.o_list_button_discard', this._onDiscardMousedown.bind(this));
+ this.$buttons.on('click', '.o_list_button_discard', this._onDiscard.bind(this));
+ if ($node) {
+ this.$buttons.appendTo($node);
}
- return Promise.resolve();
},
/**
* Overrides to update the list of selected records
@@ -214,6 +166,25 @@ var ListController = BasicController.extend({
params.selectedRecords = this.selectedRecords;
return this._super.apply(this, arguments);
},
+ /**
+ * This helper simply makes sure that the control panel buttons matches the
+ * current mode.
+ *
+ * @override
+ * @param {string} mode either 'readonly' or 'edit'
+ */
+ updateButtons: function (mode) {
+ if (!this.$buttons) {
+ return;
+ }
+ this.$buttons.toggleClass('o-editing', mode === 'edit');
+ const state = this.model.get(this.handle, {raw: true});
+ if (state.count) {
+ this.$('.o_list_export_xlsx').show();
+ } else {
+ this.$('.o_list_export_xlsx').hide();
+ }
+ },
//--------------------------------------------------------------------------
// Private
@@ -233,7 +204,7 @@ var ListController = BasicController.extend({
if ((recordID || this.handle) !== this.handle) {
var state = this.model.get(this.handle);
this.renderer.removeLine(state, recordID);
- this._updatePager();
+ this._updatePaging(state);
}
},
/**
@@ -257,7 +228,10 @@ var ListController = BasicController.extend({
self.renderer.updateState(state, {keepWidths: true})
.then(function () {
self.renderer.editRecord(recordID);
- }).then(self._updatePager.bind(self));
+ })
+ .then(() => {
+ self._updatePaging(state);
+ });
}).then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
},
/**
@@ -342,7 +316,7 @@ var ListController = BasicController.extend({
}
var self = this;
return this._super(recordID).then(function () {
- self._updateButtons('readonly');
+ self.updateButtons('readonly');
});
},
/**
@@ -357,26 +331,55 @@ var ListController = BasicController.extend({
this.getActiveDomain(), this.getSelectedIds());
},
/**
+ * Only display the pager when there are data to display.
+ *
* @override
* @private
*/
- _getSidebarEnv: function () {
- var env = this._super.apply(this, arguments);
- var record = this.model.get(this.handle);
- return _.extend(env, {
- domain: record.getDomain(),
- selectAll: !!this.$('thead .o_list_record_selector input').prop('checked'),
- });
+ _getPagingInfo: function (state) {
+ if (!state.count) {
+ return null;
+ }
+ return this._super(...arguments);
},
/**
- * Only display the pager when there are data to display.
- *
* @override
* @private
*/
- _isPagerVisible: function () {
- var state = this.model.get(this.handle, {raw: true});
- return !!state.count;
+ _getActionMenuItems: function (state) {
+ if (!this.hasActionMenus || !this.selectedRecords.length) {
+ return null;
+ }
+ const props = this._super(...arguments);
+ const otherActionItems = [{
+ description: _t("Export"),
+ callback: () => this._onExportData(),
+ }];
+ if (this.archiveEnabled) {
+ otherActionItems.push({
+ description: _t("Archive"),
+ callback: () => {
+ Dialog.confirm(this, _t("Are you sure that you want to archive all the selected records?"), {
+ confirm_callback: () => this._toggleArchiveState(true),
+ });
+ }
+ }, {
+ description: _t("Unarchive"),
+ callback: () => this._toggleArchiveState(false)
+ });
+ }
+ if (this.activeActions.delete) {
+ otherActionItems.push({
+ description: _t("Delete"),
+ callback: () => this._onDeleteSelectedRecords()
+ });
+ }
+ return Object.assign(props, {
+ items: Object.assign({}, this.toolbarActions, { other: otherActionItems }),
+ context: this.model.get(this.handle, { raw: true }).getContext(),
+ domain: state.getDomain(),
+ selectAll: this.selectAll,
+ });
},
/**
* Saves multiple records at once. This method is called by the _onFieldChanged method
@@ -406,7 +409,7 @@ var ListController = BasicController.extend({
const saveRecords = () => {
this.model.saveRecords(this.handle, recordId, validRecordIds, fieldName)
.then(async () => {
- this._updateButtons('readonly');
+ this.updateButtons('readonly');
const state = this.model.get(this.handle);
// We need to check the current multi-editable state here
// in case the selection is changed. If there are changes
@@ -481,7 +484,7 @@ var ListController = BasicController.extend({
*/
_setMode: function (mode, recordID) {
if ((recordID || this.handle) !== this.handle) {
- this._updateButtons(mode);
+ this.updateButtons(mode);
return this.renderer.setRowMode(recordID, mode);
} else {
return this._super.apply(this, arguments);
@@ -510,40 +513,13 @@ var ListController = BasicController.extend({
}
},
/**
- * Display the sidebar (the 'action' menu in the control panel) if we have
- * some selected records.
- */
- _toggleSidebar: function () {
- if (this.sidebar) {
- this.sidebar.do_toggle(this.selectedRecords.length > 0);
- }
- },
- /**
* @override
* @returns {Promise}
*/
- _update: function () {
- return this._super.apply(this, arguments)
- .then(this._toggleSidebar.bind(this))
- .then(this._toggleCreateButton.bind(this))
- .then(this._updateButtons.bind(this, 'readonly'));
- },
- /**
- * This helper simply makes sure that the control panel buttons matches the
- * current mode.
- *
- * @param {string} mode either 'readonly' or 'edit'
- */
- _updateButtons: function (mode) {
- if (this.$buttons) {
- this.$buttons.toggleClass('o-editing', mode === 'edit');
- const state = this.model.get(this.handle, {raw: true});
- if (state.count) {
- this.$('.o_list_export_xlsx').show();
- } else {
- this.$('.o_list_export_xlsx').hide();
- }
- }
+ _update: async function () {
+ await this._super(...arguments);
+ this._toggleCreateButton();
+ this.updateButtons('readonly');
},
//--------------------------------------------------------------------------
@@ -745,7 +721,8 @@ var ListController = BasicController.extend({
*/
_onSelectionChanged: function (ev) {
this.selectedRecords = ev.data.selection;
- this._toggleSidebar();
+ this.selectAll = ev.data.allChecked;
+ this._updateControlPanel();
},
/**
* If the record is set as dirty while in multiple record edition,
@@ -780,12 +757,12 @@ var ListController = BasicController.extend({
*/
_onToggleColumnOrder: function (ev) {
ev.stopPropagation();
- var data = this.model.get(this.handle);
- if (!data.groupedBy) {
- this.pager.updateState({current_min: 1});
+ var state = this.model.get(this.handle);
+ if (!state.groupedBy) {
+ this._updatePaging(state, { currentMinimum: 1 });
}
var self = this;
- this.model.setSort(data.id, ev.data.name).then(function () {
+ this.model.setSort(state.id, ev.data.name).then(function () {
self.update({});
});
},
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index cf47c997597..418c39b14e0 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -2,6 +2,7 @@ odoo.define('web.ListRenderer', function (require) {
"use strict";
var BasicRenderer = require('web.BasicRenderer');
+const { ComponentWrapper } = require('web.OwlCompatibility');
var config = require('web.config');
var core = require('web.core');
var dom = require('web.dom');
@@ -83,6 +84,7 @@ var ListRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
+
/**
* Order to focus to be given to the content of the current view
* @override
@@ -571,41 +573,6 @@ var ListRenderer = BasicRenderer.extend({
return $buttons;
},
/**
- * Renders the pager for a given group
- *
- * @private
- * @param {Object} group
- * @returns {jQueryElement} the pager's $el
- */
- _renderGroupPager: function (group) {
- var pager = new Pager(this, group.count, group.offset + 1, group.limit);
- pager.on('pager_changed', this, function (newState) {
- var self = this;
- pager.disable();
- this.trigger_up('load', {
- id: group.id,
- limit: newState.limit,
- offset: newState.current_min - 1,
- on_success: function (reloadedGroup) {
- _.extend(group, reloadedGroup);
- self._renderView();
- },
- on_fail: pager.enable.bind(pager),
- });
- });
- // register the pager so that it can be destroyed on next rendering
- this.pagers.push(pager);
-
- var pagerProm = pager._widgetRenderAndInsert(function () {}); // start the pager
- this.defs.push(pagerProm);
- var $el = $('<div>');
- pagerProm.then(function () {
- $el.replaceWith(pager.$el);
- });
-
- return $el;
- },
- /**
* Render the row that represent a group
*
* @private
@@ -670,9 +637,8 @@ var ListRenderer = BasicRenderer.extend({
$th.attr('colspan', colspanBeforeAggregate);
if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) {
- var $pager = this._renderGroupPager(group);
- var $lastCell = cells[cells.length - 1];
- $lastCell.append($pager);
+ const lastCell = cells[cells.length - 1][0];
+ this._renderGroupPager(group, lastCell);
}
if (group.isOpen && this.groupbys[groupBy]) {
var $buttons = this._renderGroupButtons(group, this.groupbys[groupBy]);
@@ -712,6 +678,30 @@ var ListRenderer = BasicRenderer.extend({
}
},
/**
+ * Renders the pager for a given group
+ *
+ * @private
+ * @param {Object} group
+ * @param {HTMLElement} target
+ */
+ _renderGroupPager: function (group, target) {
+ const currentMinimum = group.offset + 1;
+ const limit = group.limit;
+ const size = group.count;
+ if (!this._shouldRenderPager(currentMinimum, limit, size)) {
+ return;
+ }
+ const pager = new ComponentWrapper(this, Pager, { currentMinimum, limit, size });
+ const pagerMounting = pager.mount(target).then(() => {
+ // Event binding is done here to get the related group and wrapper.
+ pager.el.addEventListener('pager-changed', ev => this._onPagerChanged(ev, group));
+ // Prevent pager clicks to toggle the group.
+ pager.el.addEventListener('click', ev => ev.stopPropagation());
+ });
+ this.defs.push(pagerMounting);
+ this.pagers.push(pager);
+ },
+ /**
* Render all groups in the view. We assume that the view is in grouped
* mode.
*
@@ -937,7 +927,7 @@ var ListRenderer = BasicRenderer.extend({
_renderView: function () {
var self = this;
- var oldPagers = this.pagers;
+ const oldPagers = this.pagers;
this.pagers = [];
// display the no content helper if there is no data to display
@@ -945,7 +935,7 @@ var ListRenderer = BasicRenderer.extend({
this.$el.toggleClass('o_list_view', !displayNoContentHelper);
if (displayNoContentHelper) {
// destroy the previously instantiated pagers, if any
- _.invoke(oldPagers, 'destroy');
+ oldPagers.forEach(pager => pager.destroy());
this.$el.removeClass('table-responsive');
this.$el.html(this._renderNoContentHelper());
@@ -971,16 +961,22 @@ var ListRenderer = BasicRenderer.extend({
$table.append(this._renderBody());
$table.append(this._renderFooter());
}
+ const tableWrapper = Object.assign(document.createElement('div'), {
+ className: 'table-responsive',
+ });
+ tableWrapper.appendChild($table[0]);
delete this.defs;
- var prom = Promise.all(defs).then(function () {
+ var prom = Promise.all(defs).then(() => {
// destroy the previously instantiated pagers, if any
- _.invoke(oldPagers, 'destroy');
+ oldPagers.forEach(pager => pager.destroy());
- self.$el.html($('<div>', {
- class: 'table-responsive',
- html: $table
- }));
+ // Append the table to the main element
+ self.el.innerHTML = "";
+ dom.append(self.el, tableWrapper, {
+ callbacks: [{ widget: this }],
+ in_DOM: document.body.contains(self.el),
+ });
if (self.optionalColumns.length) {
self.$el.addClass('o_list_optional_columns');
@@ -1016,6 +1012,18 @@ var ListRenderer = BasicRenderer.extend({
});
},
/**
+ * @private
+ * @returns {boolean}
+ */
+ _shouldRenderPager: function (currentMinimum, limit, size) {
+ if (!limit || !size) {
+ return false;
+ }
+ const maximum = Math.min(currentMinimum + limit - 1, size);
+ const singlePage = (1 === currentMinimum) && (maximum === size);
+ return !singlePage;
+ },
+ /**
* Update the footer aggregate values. This method should be called each
* time the state of some field is changed, to make sure their sum are kept
* in sync.
@@ -1046,7 +1054,7 @@ var ListRenderer = BasicRenderer.extend({
}
});
this.$('thead .o_list_record_selector input').prop('checked', allChecked);
- this.trigger_up('selection_changed', { selection: this.selection });
+ this.trigger_up('selection_changed', { allChecked, selection: this.selection });
this._updateFooter();
},
@@ -1212,6 +1220,24 @@ var ListRenderer = BasicRenderer.extend({
},
/**
* @private
+ * @param {OwlEvent} ev
+ * @param {Object} group
+ */
+ _onPagerChanged: async function (ev, group) {
+ ev.stopPropagation();
+ const { currentMinimum, limit } = ev.detail;
+ this.trigger_up('load', {
+ id: group.id,
+ limit: limit,
+ offset: currentMinimum - 1,
+ on_success: reloadedGroup => {
+ Object.assign(group, reloadedGroup);
+ this._renderView();
+ },
+ });
+ },
+ /**
+ * @private
* @param {MouseEvent} ev
*/
_onRowClicked: function (ev) {
@@ -1246,7 +1272,6 @@ var ListRenderer = BasicRenderer.extend({
*/
_onToggleGroup: function (ev) {
ev.preventDefault();
- ev.stopPropagation();
var group = $(ev.currentTarget).closest('tr').data('group');
if (group.count) {
this.trigger_up('toggle_group', {
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index bb6dfed8116..5845375fd70 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -33,7 +33,7 @@ var ListView = BasicView.extend({
*
* @param {Object} viewInfo
* @param {Object} params
- * @param {boolean} params.hasSidebar
+ * @param {boolean} params.hasActionMenus
* @param {boolean} [params.hasSelectors=true]
*/
init: function (viewInfo, params) {
@@ -58,7 +58,7 @@ var ListView = BasicView.extend({
this.controllerParams.activeActions.export_xlsx = this.arch.attrs.export_xlsx ? !!JSON.parse(this.arch.attrs.export_xlsx): true;
this.controllerParams.editable = editable;
- this.controllerParams.hasSidebar = params.hasSidebar;
+ this.controllerParams.hasActionMenus = params.hasActionMenus;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.mode = editable ? 'edit' : 'readonly';
this.controllerParams.selectedRecords = selectedRecords;
@@ -101,7 +101,7 @@ var ListView = BasicView.extend({
var params = this._super.apply(this, arguments);
var inDialog = action.target === 'new';
var inline = action.target === 'inline';
- params.hasSidebar = !inDialog && !inline;
+ params.hasActionMenus = !inDialog && !inline;
return params;
},
/**
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index c54c6affedc..ee37aba3ce5 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -85,25 +85,44 @@ odoo.define('web.PivotController', function (require) {
* add listeners on it.
* Set this.$buttons with the produced jQuery element
*
+ * @override
* @param {jQuery} [$node] a jQuery node where the rendered buttons should
* be inserted. $node may be undefined, in which case the PivotView
* does nothing
*/
renderButtons: function ($node) {
+ const context = {
+ measures: Object.entries(this.measures)
+ .filter(x => x[0] !== '__count')
+ .sort((a, b) => a[1].string.toLowerCase() > b[1].string.toLowerCase() ? 1 : -1),
+ };
+ this.$buttons = $(QWeb.render('PivotView.buttons', context));
+ this.$buttons.click(this._onButtonClick.bind(this));
+ this.$buttons.find('button').tooltip();
if ($node) {
- const context = {
- measures: Object.entries(this.measures)
- .filter(x => x[0] !== '__count')
- .sort((a, b) => a[1].string.toLowerCase() > b[1].string.toLowerCase() ? 1 : -1),
- };
- this.$buttons = $(QWeb.render('PivotView.buttons', context));
- this.$buttons.click(this._onButtonClick.bind(this));
- this.$buttons.find('button').tooltip();
-
this.$buttons.appendTo($node);
- this._updateButtons();
}
},
+ /**
+ * @override
+ */
+ updateButtons: function () {
+ if (!this.$buttons) {
+ return;
+ }
+ const state = this.model.get({ raw: true });
+ Object.entries(this.measures).forEach(elt => {
+ const name = elt[0];
+ const isSelected = state.measures.includes(name);
+ this.$buttons.find('.dropdown-item[data-field="' + name + '"]')
+ .toggleClass('selected', isSelected);
+
+ });
+ const noDataDisplayed = !state.hasData || !state.measures.length;
+ this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed);
+ this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed);
+ this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed);
+ },
//--------------------------------------------------------------------------
// Private
@@ -131,34 +150,6 @@ odoo.define('web.PivotController', function (require) {
error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
- /**
- * @override
- * @private
- */
- _update: function () {
- this._updateButtons();
- return this._super.apply(this, arguments);
- },
- /**
- * @private
- */
- _updateButtons: function () {
- if (!this.$buttons) {
- return;
- }
- const state = this.model.get({ raw: true });
- Object.entries(this.measures).forEach(elt => {
- const name = elt[0];
- const isSelected = state.measures.includes(name);
- this.$buttons.find('.dropdown-item[data-field="' + name + '"]')
- .toggleClass('selected', isSelected);
-
- });
- const noDataDisplayed = !state.hasData || !state.measures.length;
- this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed);
- this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed);
- this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed);
- },
//--------------------------------------------------------------------------
// Handlers
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 67122ceb40d..8e4f92a4d75 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -530,18 +530,15 @@ var PivotModel = AbstractModel.extend({
* @param {boolean} [params.compare=false]
* @param {Object} params.context
* @param {Object} params.fields
- * @param {Array[]} [params.comparisonTimeRange=[]]
* @param {string[]} [params.groupedBy]
- * @param {Array[]} [params.timeRange=[]]
* @param {string[]} params.colGroupBys
* @param {Array[]} params.domain
* @param {string[]} params.measures
* @param {string[]} params.rowGroupBys
- * @param {string} [params.comparisonTimeRangeDescription=""]
* @param {string} [params.default_order]
- * @param {string} [params.timeRangeDescription=""]
* @param {string} params.modelName
* @param {Object[]} params.groupableFields
+ * @param {Object} params.timeRanges
* @returns {Promise}
*/
load: function (params) {
@@ -556,19 +553,14 @@ var PivotModel = AbstractModel.extend({
expandedRowGroupBys: [],
expandedColGroupBys: [],
domain: this.initialDomain,
- timeRange: params.timeRange || [],
- timeRangeDescription: params.timeRangeDescription || "",
- comparisonTimeRange: params.comparisonTimeRange || [],
- comparisonTimeRangeDescription: params.comparisonTimeRangeDescription || "",
- compare: params.compare || false,
context: _.extend({}, session.user_context, params.context),
groupedBy: params.context.pivot_row_groupby || params.groupedBy,
colGroupBys: params.context.pivot_column_groupby || params.colGroupBys,
measures: this._processMeasures(params.context.pivot_measures) || params.measures,
+ timeRanges: params.timeRanges,
};
+ this._computeDerivedParams();
- this.data.domains = this._getDomains();
- this.data.origins = this._getOrigins();
this.data.rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys;
var defaultOrder = params.default_order && params.default_order.split(' ');
@@ -588,14 +580,11 @@ var PivotModel = AbstractModel.extend({
* @param {Object} params
* @param {boolean} [params.compare=false]
* @param {Object} params.context
- * @param {Array[]} [params.comparisonTimeRange=[]]
* @param {string[]} [params.groupedBy]
- * @param {Array[]} [params.timeRange=[]]
* @param {Array[]} params.domain
* @param {string[]} params.groupBy
* @param {string[]} params.measures
- * @param {string} [params.comparisonTimeRangeDescription=""]
- * @param {string} [params.timeRangeDescription=""]
+ * @param {Object} [params.timeRanges]
* @returns {Promise}
*/
reload: function (handle, params) {
@@ -608,21 +597,6 @@ var PivotModel = AbstractModel.extend({
this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy;
this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures;
this.defaultGroupedBy = this.data.groupedBy.length ? this.data.groupedBy : this.defaultGroupedBy;
- var timeRangeMenuData = params.context.timeRangeMenuData;
- if (timeRangeMenuData) {
- this.data.timeRange = timeRangeMenuData.timeRange || [];
- this.data.timeRangeDescription = timeRangeMenuData.timeRangeDescription || "";
- this.data.comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
- this.data.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription || "";
- this.data.compare = this.data.comparisonTimeRange.length > 0;
- } else {
- this.data.timeRange = [];
- this.data.timeRangeDescription = "";
- this.data.comparisonTimeRange = [];
- this.data.comparisonTimeRangeDescription = "";
- this.data.compare = false;
- this.data.context = _.omit(this.data.context, 'timeRangeMenuData');
- }
}
if ('domain' in params) {
this.data.domain = params.domain;
@@ -633,9 +607,11 @@ var PivotModel = AbstractModel.extend({
if ('groupBy' in params) {
this.data.groupedBy = params.groupBy.length ? params.groupBy : this.defaultGroupedBy;
}
+ if ('timeRanges' in params) {
+ this.data.timeRanges = params.timeRanges;
+ }
+ this._computeDerivedParams();
- this.data.domains = this._getDomains();
- this.data.origins = this._getOrigins();
this.data.rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys;
if (!_.isEqual(oldRowGroupBys, self.data.rowGroupBys)) {
@@ -792,20 +768,6 @@ var PivotModel = AbstractModel.extend({
}
},
/**
- * Returns the principal domains used by the pivot model to fetch data.
- * The domains represent two main groups of records.
- *
- * @private
- * @returns {Array[][]}
- */
- _getDomains: function () {
- var domains = [this.data.domain.concat(this.data.timeRange)];
- if (this.data.compare) {
- domains.push(this.data.domain.concat(this.data.comparisonTimeRange));
- }
- return domains;
- },
- /**
* Returns the rowGroupBys and colGroupBys arrays that
* are actually used by the pivot view internally
* (for read_group or other purpose)
@@ -1083,19 +1045,6 @@ var PivotModel = AbstractModel.extend({
return originRow;
},
- /**
- * Create an array with the origin descriptions.
- *
- * @private
- * @returns {string[]}
- */
- _getOrigins: function () {
- var origins = [this.data.timeRangeDescription || ""];
- if (this.data.compare) {
- origins.push(this.data.comparisonTimeRangeDescription);
- }
- return origins;
- },
/**
* Get the selection needed to display the group by dropdown
@@ -1438,6 +1387,26 @@ var PivotModel = AbstractModel.extend({
}
},
/**
+ * Determine this.data.domains and this.data.origins from
+ * this.data.domain and this.data.timeRanges;
+ *
+ * @private
+ */
+ _computeDerivedParams: function () {
+ const { range, rangeDescription, comparisonRange, comparisonRangeDescription } = this.data.timeRanges;
+ if (range) {
+ this.data.domains = [this.data.domain.concat(range)];
+ this.data.origins = [rangeDescription];
+ if (comparisonRange) {
+ this.data.domains.push(this.data.domain.concat(comparisonRange));
+ this.data.origins.push(comparisonRangeDescription);
+ }
+ } else {
+ this.data.domains = [this.data.domain];
+ this.data.origins = [""];
+ }
+ },
+ /**
* Make any group in tree a leaf if it was a leaf in oldTree.
*
* @private
diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js
index bd259d9d0aa..1fca1e14487 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -18,8 +18,8 @@ odoo.define('web.PivotView', function (require) {
const _t = core._t;
const _lt = core._lt;
- const controlPanelViewParameters = require('web.controlPanelViewParameters');
- const GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES;
+ const searchUtils = require('web.searchUtils');
+ const GROUPABLE_TYPES = searchUtils.GROUPABLE_TYPES;
const PivotView = AbstractView.extend({
display_name: _lt('Pivot'),
@@ -141,7 +141,7 @@ odoo.define('web.PivotView', function (require) {
*/
getRenderer(parent, state) {
state = Object.assign(state || {}, this.rendererParams);
- return new RendererWrapper(null, this.config.Renderer, state);
+ return new RendererWrapper(parent, this.config.Renderer, state);
},
});
diff --git a/addons/web/static/src/js/views/qweb/qweb_view.js b/addons/web/static/src/js/views/qweb/qweb_view.js
index 0c858b5eb6c..21ad867c286 100644
--- a/addons/web/static/src/js/views/qweb/qweb_view.js
+++ b/addons/web/static/src/js/views/qweb/qweb_view.js
@@ -91,16 +91,21 @@ var Renderer = AbstractRenderer.extend({
var Controller = AbstractController.extend({
events: _.extend({}, AbstractController.prototype.events, {
'click [type="toggle"]': '_onLazyToggle',
+ 'click [type="action"]' : '_onActionClicked',
}),
init: function () {
this._super.apply(this, arguments);
- this._$buttons = $('<nav>').on('click', '[type="action"]', this.proxy('_onActionClicked'));
},
+ /**
+ * @override
+ */
renderButtons: function ($node) {
- this._super($node);
- $node.append(this._$buttons);
+ this.$buttons = $('<nav/>');
+ if ($node) {
+ $node.append(this.$buttons);
+ }
},
_update: function () {
var self = this;
@@ -108,7 +113,7 @@ var Controller = AbstractController.extend({
// move control panel buttons from the view to the control panel
// area
var $cp_buttons = self.renderer.$('nav.o_qweb_cp_buttons');
- $cp_buttons.children().appendTo(self._$buttons.empty());
+ $cp_buttons.children().appendTo(self.$buttons.empty());
$cp_buttons.remove();
});
},
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js
index 4f8c42325db..590d52e4941 100644
--- a/addons/web/static/src/js/views/search_panel.js
+++ b/addons/web/static/src/js/views/search_panel.js
@@ -165,27 +165,26 @@ var SearchPanel = Widget.extend({
* @params {string} viewType the type of the current view (e.g. 'kanban')
* @returns {Object|undefined}
*/
- computeSearchPanelParams: function (viewInfo, viewType) {
+ computeSearchPanelParams: function (arch, fields, viewType) {
var searchPanelSections;
var classes;
- if (viewInfo) {
- var arch = viewUtils.parseArch(viewInfo.arch);
+ if (arch && fields) {
viewType = viewType === 'list' ? 'tree' : viewType;
- arch.children.forEach(function (node) {
- if (node.tag === 'searchpanel') {
- var attrs = node.attrs;
- var viewTypes = defaultViewTypes;
- if (attrs.view_types) {
- viewTypes = attrs.view_types.split(',');
- }
- if (attrs.class) {
- classes = attrs.class.split(' ');
- }
- if (viewTypes.indexOf(viewType) !== -1) {
- searchPanelSections = _processSearchPanelNode(node, viewInfo.fields);
- }
+ arch = viewUtils.parseArch(arch);
+ const searchPanelNode = arch.children.find(child => child.tag === 'searchpanel');
+ if (searchPanelNode) {
+ var attrs = searchPanelNode.attrs;
+ var viewTypes = defaultViewTypes;
+ if (attrs.view_types) {
+ viewTypes = attrs.view_types.split(',');
}
- });
+ if (attrs.class) {
+ classes = attrs.class.split(' ');
+ }
+ if (viewTypes.includes(viewType)) {
+ searchPanelSections = _processSearchPanelNode(searchPanelNode, fields);
+ }
+ }
}
return {
sections: searchPanelSections,
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index 47e3a5309d9..6f00b60d2a4 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -220,6 +220,7 @@ var FormViewDialog = ViewDialog.extend({
callbacks: [{widget: self.form_view}],
in_DOM: true,
});
+ self.form_view.updateButtons();
});
return _super();
});