summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/chrome/abstract_action.js115
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js14
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js2
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js11
-rw-r--r--addons/web/static/src/js/chrome/action_mixin.js72
-rw-r--r--addons/web/static/src/js/components/custom_file_input.js2
-rw-r--r--addons/web/static/src/js/core/custom_hooks.js52
-rw-r--r--addons/web/static/src/js/core/owl_dialog.js4
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js155
-rw-r--r--addons/web/static/src/js/report/client_action.js22
-rw-r--r--addons/web/static/src/js/services/data_manager.js10
-rw-r--r--addons/web/static/src/js/tools/debug_manager_backend.js2
-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
-rw-r--r--addons/web/static/src/js/widgets/pie_chart.js3
-rw-r--r--addons/web/static/src/xml/debug.xml2
38 files changed, 1301 insertions, 1410 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_action.js b/addons/web/static/src/js/chrome/abstract_action.js
index e28fd896182..b300cb22cf4 100644
--- a/addons/web/static/src/js/chrome/abstract_action.js
+++ b/addons/web/static/src/js/chrome/abstract_action.js
@@ -9,12 +9,14 @@ odoo.define('web.AbstractAction', function (require) {
*/
var ActionMixin = require('web.ActionMixin');
-var ControlPanelView = require('web.ControlPanelView');
+var ControlPanel = require('web.ControlPanel');
+var ControlPanelModel = require('web.ControlPanelModel');
var Widget = require('web.Widget');
+const { ComponentWrapper } = require('web.OwlCompatibility');
var AbstractAction = Widget.extend(ActionMixin, {
config: {
- ControlPanelView: ControlPanelView,
+ ControlPanel: ControlPanel,
},
/**
@@ -33,7 +35,7 @@ var AbstractAction = Widget.extend(ActionMixin, {
* For example, the Discuss application adds the following line in its
* constructor::
*
- * this.controlPanelParams.modelName = 'mail.message';
+ * this.controlPanelModelConfig.modelName = 'mail.message';
*
* @type boolean
*/
@@ -73,15 +75,25 @@ var AbstractAction = Widget.extend(ActionMixin, {
init: function (parent, action, options) {
this._super(parent);
this._title = action.display_name || action.name;
- this.controlPanelParams = {
- actionId: action.id,
- context: action.context,
- breadcrumbs: options && options.breadcrumbs || [],
- title: this.getTitle(),
- viewId: action.search_view_id && action.search_view_id[0],
- withSearchBar: this.withSearchBar,
- searchMenuTypes: this.searchMenuTypes,
- };
+
+ if (this.hasControlPanel) {
+ this.controlPanelModelConfig = {
+ actionId: action.id,
+ actionContext: action.context || {},
+ actionDomain: action.domain || [],
+ env: owl.Component.env,
+ withSearchBar: this.withSearchBar,
+ };
+
+ this.viewId = action.search_view_id && action.search_view_id[0];
+
+ this.controlPanelProps = {
+ action,
+ breadcrumbs: options && options.breadcrumbs,
+ withSearchBar: this.withSearchBar,
+ searchMenuTypes: this.searchMenuTypes,
+ };
+ }
},
/**
* The willStart method is actually quite complicated if the client action
@@ -89,40 +101,73 @@ var AbstractAction = Widget.extend(ActionMixin, {
*
* @override
*/
- willStart: function () {
- var self = this;
- var proms = [this._super.apply(this, arguments)];
+ willStart: async function () {
+ const proms = [this._super(...arguments)];
if (this.hasControlPanel) {
- var params = this.controlPanelParams;
if (this.loadControlPanel) {
- proms.push(this
- .loadFieldView(params.modelName, params.context || {}, params.viewId, 'search')
- .then(function (fieldsView) {
- params.viewInfo = {
- arch: fieldsView.arch,
- fields: fieldsView.fields,
- };
- }));
+ const { context, searchMenuTypes } = this.controlPanelProps;
+ const { modelName } = this.controlPanelModelConfig;
+ const options = { load_filters: searchMenuTypes.includes('favorite') };
+ const args = [modelName, context || {}, this.viewId, 'search', options];
+ const loadFieldViewPromise = this.loadFieldView(...args);
+ const {arch, fields, favoriteFilters } = await loadFieldViewPromise;
+ this.controlPanelModelConfig.viewInfo = {arch, fields, favoriteFilters };
+ this.controlPanelProps.fields = fields;
}
- return Promise.all(proms).then(function () {
- var controlPanelView = new self.config.ControlPanelView(params);
- return controlPanelView.getController(self).then(function (controlPanel) {
- self._controlPanel = controlPanel;
- return self._controlPanel.appendTo(document.createDocumentFragment());
- });
- });
+ this._controlPanelModel = new ControlPanelModel(this.controlPanelModelConfig);
+ this.controlPanelProps.controlPanelModel = this._controlPanelModel;
+ proms.push(this._controlPanelModel.isReady);
}
return Promise.all(proms);
},
/**
* @override
*/
- start: function () {
- if (this._controlPanel) {
- this._controlPanel.$el.prependTo(this.$el);
+ start: async function () {
+ await this._super(...arguments);
+ if (this.hasControlPanel) {
+ if ('title' in this.controlPanelProps) {
+ this._setTitle(this.controlPanelProps.title);
+ }
+ this.controlPanelProps.title = this.getTitle();
+ this._controlPanelWrapper = new ComponentWrapper(this, ControlPanel, this.controlPanelProps);
+ await this._controlPanelWrapper.mount(this.el, { position: 'first-child' });
+
}
- return this._super.apply(this, arguments);
},
+ /**
+ * @override
+ */
+ destroy: function() {
+ this._super.apply(this, arguments);
+ ActionMixin.destroy.call(this);
+ },
+ /**
+ * @override
+ */
+ on_attach_callback: function () {
+ ActionMixin.on_attach_callback.call(this);
+ if (this.hasControlPanel) {
+ this._controlPanelModel.on('search', this, this._onSearch);
+ this._controlPanelModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams);
+ }
+ },
+ /**
+ * @override
+ */
+ on_detach_callback: function () {
+ ActionMixin.on_detach_callback.call(this);
+ if (this.hasControlPanel) {
+ this._controlPanelModel.off('search', this);
+ this._controlPanelModel.off('get-controller-query-params', this);
+ }
+ },
+
+ /**
+ * @private
+ * @param {Object} [searchQuery]
+ */
+ _onSearch: function () {},
});
return AbstractAction;
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 8837ffe813e..9012b7f53fd 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -32,9 +32,7 @@ var _t = core._t;
var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
dependencies: ['notification'],
- events: _.extend({}, KeyboardNavigationMixin.events, {
- 'click .o_search_options .dropdown-menu': '_onClickDropDownMenu',
- }),
+ events: _.extend({}, KeyboardNavigationMixin.events),
custom_events: {
call_service: '_onCallService',
clear_uncommitted_changes: function (e) {
@@ -357,16 +355,6 @@ var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
payload.callback(result);
},
/**
- * When clicking inside a dropdown to modify search options
- * prevents the bootstrap dropdown to close on itself
- *
- * @private
- * @param {Event} ev
- */
- _onClickDropDownMenu: function (ev) {
- ev.stopPropagation();
- },
- /**
* Whenever the connection is lost, we need to notify the user.
*
* @private
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index ee108c94326..92c090827d8 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -877,7 +877,7 @@ var ActionManager = Widget.extend({
/**
* @private
* @param {OdooEvent} ev
- * @param {OdooEvent} ev.data.controllerID
+ * @param {string} ev.data.controllerID
*/
_onBreadcrumbClicked: function (ev) {
ev.stopPropagation();
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index ff060153f27..f6c5661eb8e 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -546,6 +546,7 @@ ActionManager.include({
return Promise.resolve(controller.widget.willRestore()).then(function () {
viewOptions = _.extend({}, viewOptions, {
breadcrumbs: self._getBreadcrumbs(self.controllerStack.slice(0, index)),
+ shouldUpdateControlPanel: true,
});
return controller.widget.reload(viewOptions).then(function () {
return controller;
@@ -708,15 +709,15 @@ ActionManager.include({
*/
_onSwitchView: function (ev) {
ev.stopPropagation();
- var viewType = ev.data.view_type;
- var currentController = this.getCurrentController();
+ const viewType = ev.data.view_type;
+ const currentController = this.getCurrentController();
if (currentController.jsID === ev.data.controllerID) {
// only switch to the requested view if the controller that
// triggered the request is the current controller
- var action = this.actions[currentController.actionID];
- var currentControllerState = currentController.widget.exportState();
+ const action = this.actions[currentController.actionID];
+ const currentControllerState = currentController.widget.exportState();
action.controllerState = _.extend({}, action.controllerState, currentControllerState);
- var options = {
+ const options = {
controllerState: action.controllerState,
currentId: ev.data.res_id,
};
diff --git a/addons/web/static/src/js/chrome/action_mixin.js b/addons/web/static/src/js/chrome/action_mixin.js
index a704e5736a0..99f45405c72 100644
--- a/addons/web/static/src/js/chrome/action_mixin.js
+++ b/addons/web/static/src/js/chrome/action_mixin.js
@@ -18,11 +18,13 @@ odoo.define('web.ActionMixin', function (require) {
* action manager.
*
* @module web.ActionMixin
+ * @extends WidgetAdapterMixin
*/
const core = require('web.core');
+ const { WidgetAdapterMixin } = require('web.OwlCompatibility');
- const ActionMixin = {
+ const ActionMixin = Object.assign({}, WidgetAdapterMixin, {
template: 'Action',
/**
@@ -41,7 +43,7 @@ odoo.define('web.ActionMixin', function (require) {
* will also implement the ParentedMixin which actually manages those
*/
custom_events: {
- get_controller_query_params: '_onGetControllerQueryParams',
+ get_controller_query_params: '_onGetOwnedQueryParams',
},
/**
@@ -64,16 +66,6 @@ odoo.define('web.ActionMixin', function (require) {
_title: '',
/**
- * Called each time the action is attached into the DOM.
- */
- on_attach_callback: function () { },
-
- /**
- * Called each time the action is detached from the DOM.
- */
- on_detach_callback: function () { },
-
- /**
* @override
*/
renderElement: function () {
@@ -177,21 +169,40 @@ odoo.define('web.ActionMixin', function (require) {
renderButtons: function ($node) { },
/**
- * This is the main method to customize the controlpanel content.
- *
- * @see updateContents method in ControlPanelRenderer for more info
+ * Method used to update the widget buttons state.
+ */
+ updateButtons: function () { },
+
+ /**
+ * The parameter newProps is used to update the props of
+ * the controlPanelWrapper before render it. The key 'cp_content'
+ * is not a prop of the control panel itself. One should if possible use
+ * the slot mechanism.
*
- * @param {Object} [status]
- * @param {string} [status.title]
- * @param {Object} [options]
- * @param {boolean} [options.clear]
- */
- updateControlPanel: function (status, options) {
- if (this._controlPanel) {
- status = status || {};
- status.title = status.title || this.getTitle();
- this._controlPanel.updateContents(status, options || {});
+ * @param {Object} [newProps={}]
+ * @returns {Promise}
+ */
+ updateControlPanel: async function (newProps = {}) {
+ if (!this.withControlPanel && !this.hasControlPanel) {
+ return;
+ }
+ const props = Object.assign({}, newProps); // Work with a clean new object
+ if ('title' in props) {
+ this._setTitle(props.title);
+ this.controlPanelProps.title = this.getTitle();
+ delete props.title;
+ }
+ if ('cp_content' in props) {
+ // cp_content has been updated: refresh it.
+ this.controlPanelProps.cp_content = Object.assign({},
+ this.controlPanelProps.cp_content,
+ props.cp_content,
+ );
+ delete props.cp_content;
}
+ // Update props state
+ Object.assign(this.controlPanelProps, props);
+ return this._controlPanelWrapper.update(this.controlPanelProps);
},
//---------------------------------------------------------------------
@@ -204,7 +215,6 @@ odoo.define('web.ActionMixin', function (require) {
*/
_setTitle: function (title) {
this._title = title;
- this.updateControlPanel({ title: this._title }, { clear: false });
},
//---------------------------------------------------------------------
@@ -218,15 +228,13 @@ odoo.define('web.ActionMixin', function (require) {
* current controller.
*
* @private
- * @param {OdooEvent} ev
- * @param {function} ev.data.callback used to send the requested state
+ * @param {function} callback used to send the requested state
*/
- _onGetControllerQueryParams: function (ev) {
- ev.stopPropagation();
+ _onGetOwnedQueryParams: function (callback) {
const state = this.getOwnedQueryParams();
- ev.data.callback(state || {});
+ callback(state || {});
},
- };
+ });
return ActionMixin;
});
diff --git a/addons/web/static/src/js/components/custom_file_input.js b/addons/web/static/src/js/components/custom_file_input.js
index eb9c4267cfc..14f521a4ed6 100644
--- a/addons/web/static/src/js/components/custom_file_input.js
+++ b/addons/web/static/src/js/components/custom_file_input.js
@@ -82,7 +82,7 @@ odoo.define('web.CustomFileInput', function (require) {
model: { type: String, optional: 1 },
multi_upload: { type: Boolean, optional: 1 },
};
- CustomFileInput.template = 'CustomFileInput';
+ CustomFileInput.template = 'web.CustomFileInput';
return CustomFileInput;
});
diff --git a/addons/web/static/src/js/core/custom_hooks.js b/addons/web/static/src/js/core/custom_hooks.js
index 69750066a8c..883f385ac3c 100644
--- a/addons/web/static/src/js/core/custom_hooks.js
+++ b/addons/web/static/src/js/core/custom_hooks.js
@@ -5,35 +5,37 @@ odoo.define('web.custom_hooks', function () {
const { onMounted, onPatched, onWillUnmount } = hooks;
/**
- * Returns a function which purpose is to focus the given selector on the next
- * repaint (mount or patch). Its default selector is the first element having
- * an `autofocus' attribute. Text selection will be set at the end of the value
- * if the target is a text element. The action is lost if no element was found.
- *
- * @returns {Function}
+ * Focus a given selector as soon as it appears in the DOM and if it was not
+ * displayed before. If the selected target is an input|textarea, set the selection
+ * at the end.
+ * @param {Object} [params]
+ * @param {string} [params.selector='autofocus'] default: select the first element
+ * with an `autofocus` attribute.
+ * @returns {Function} function that forces the focus on the next update if visible.
*/
- function useFocusOnUpdate() {
- const component = Component.current;
- let willFocus = null;
-
- function _focusSelector() {
- if (willFocus) {
- const target = component.el.querySelector(willFocus);
- if (target) {
- target.focus();
- if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
- target.selectionStart = target.selectionEnd = target.value.length;
- }
+ function useAutofocus(params = {}) {
+ const comp = Component.current;
+ // Prevent autofocus in mobile
+ if (comp.env.device.isMobile) {
+ return () => {};
+ }
+ const selector = params.selector || '[autofocus]';
+ let target = null;
+ function autofocus() {
+ const prevTarget = target;
+ target = comp.el.querySelector(selector);
+ if (target && target !== prevTarget) {
+ target.focus();
+ if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
+ target.selectionStart = target.selectionEnd = target.value.length;
}
- willFocus = null;
}
}
+ onMounted(autofocus);
+ onPatched(autofocus);
- onMounted(_focusSelector);
- onPatched(_focusSelector);
-
- return function focusOnUpdate(selector = '[autofocus]') {
- willFocus = selector;
+ return function focusOnUpdate() {
+ target = null;
};
}
@@ -110,7 +112,7 @@ odoo.define('web.custom_hooks', function () {
}
return {
- useFocusOnUpdate,
+ useAutofocus,
useListener,
};
});
diff --git a/addons/web/static/src/js/core/owl_dialog.js b/addons/web/static/src/js/core/owl_dialog.js
index d376e042ae4..daa29468934 100644
--- a/addons/web/static/src/js/core/owl_dialog.js
+++ b/addons/web/static/src/js/core/owl_dialog.js
@@ -163,7 +163,7 @@ odoo.define('web.OwlDialog', function () {
$(this.mainButton)
.tooltip({
delay: { show: 200, hide: 0 },
- title: () => this.env.qweb.renderToString('DialogButton.tooltip', {
+ title: () => this.env.qweb.renderToString('web.DialogButton.tooltip', {
title: this.mainButton.innerText.toUpperCase(),
}),
trigger: 'manual',
@@ -263,7 +263,7 @@ odoo.define('web.OwlDialog', function () {
technical: Boolean,
title: String,
};
- Dialog.template = 'OwlDialog';
+ Dialog.template = 'web.OwlDialog';
return Dialog;
});
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 705a0c872fa..667c2a1b6ca 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -16,7 +16,7 @@ odoo.define('web.relational_fields', function (require) {
var AbstractField = require('web.AbstractField');
var basicFields = require('web.basic_fields');
var concurrency = require('web.concurrency');
-var ControlPanelView = require('web.ControlPanelView');
+const ControlPanelX2Many = require('web.ControlPanelX2Many');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
@@ -26,7 +26,7 @@ const Domain = require('web.Domain');
var KanbanRecord = require('web.KanbanRecord');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
-var Pager = require('web.Pager');
+const { ComponentWrapper, WidgetAdapterMixin } = require('web.OwlCompatibility');
var _t = core._t;
var _lt = core._lt;
@@ -963,7 +963,7 @@ var KanbanFieldMany2One = AbstractField.extend({
// X2Many widgets
//------------------------------------------------------------------------------
-var FieldX2Many = AbstractField.extend({
+var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, {
tagName: 'div',
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
add_record: '_onAddRecord',
@@ -980,6 +980,7 @@ var FieldX2Many = AbstractField.extend({
navigation_move: '_onNavigationMove',
save_optional_fields: '_onSaveOrLoadOptionalFields',
load_optional_fields: '_onSaveOrLoadOptionalFields',
+ pager_changed: '_onPagerChanged',
}),
// We need to trigger the reset on every changes to be aware of the parent changes
@@ -1006,6 +1007,21 @@ var FieldX2Many = AbstractField.extend({
this.isMany2Many = this.field.type === 'many2many' || this.attrs.widget === 'many2many';
this.activeActions = {};
this.recordParams = {fieldName: this.name, viewType: this.viewType};
+ // The limit is fixed so it cannot be changed by adding/removing lines in
+ // the widget. It will only change through a hard reload or when manually
+ // changing the pager (see _onPagerChanged).
+ this.pagingState = {
+ currentMinimum: this.value.offset + 1,
+ limit: this.value.limit,
+ size: this.value.count,
+ validate: () => {
+ // TODO: we should have some common method in the basic renderer...
+ return this.view.arch.tag === 'tree' ?
+ this.renderer.unselectRow() :
+ Promise.resolve();
+ },
+ withAccessKey: false,
+ };
var arch = this.view && this.view.arch;
if (arch) {
this.activeActions.create = arch.attrs.create ?
@@ -1024,8 +1040,21 @@ var FieldX2Many = AbstractField.extend({
/**
* @override
*/
- start: function () {
- return this._renderControlPanel().then(this._super.bind(this));
+ start: async function () {
+ const _super = this._super.bind(this);
+ if (this.view) {
+ this._renderButtons();
+ this._controlPanelWrapper = new ComponentWrapper(this, ControlPanelX2Many, {
+ cp_content: { $buttons: this.$buttons },
+ pager: this.pagingState,
+ });
+ await this._controlPanelWrapper.mount(this.el, { position: 'first-child' });
+ }
+ return _super(...arguments);
+ },
+ destroy: function () {
+ WidgetAdapterMixin.destroy.call(this);
+ this._super();
},
/**
* For the list renderer to properly work, it must know if it is in the DOM,
@@ -1033,6 +1062,7 @@ var FieldX2Many = AbstractField.extend({
*/
on_attach_callback: function () {
this.isInDOM = true;
+ WidgetAdapterMixin.on_attach_callback.call(this);
if (this.renderer) {
this.renderer.on_attach_callback();
}
@@ -1042,6 +1072,10 @@ var FieldX2Many = AbstractField.extend({
*/
on_detach_callback: function () {
this.isInDOM = false;
+ WidgetAdapterMixin.on_detach_callback.call(this);
+ if (this.renderer) {
+ this.renderer.on_detach_callback();
+ }
},
//--------------------------------------------------------------------------
@@ -1116,7 +1150,10 @@ var FieldX2Many = AbstractField.extend({
}
}
return this._super.apply(this, arguments).then(() => {
- this._updateControlPanel();
+ if (this.view) {
+ this._renderButtons();
+ this._updateControlPanel();
+ }
});
},
@@ -1249,8 +1286,8 @@ var FieldX2Many = AbstractField.extend({
addTrashIcon: this._hasTrashIcon(),
columnInvisibleFields: this.currentColInvisibleFields,
keepWidths: true,
- }).then(function () {
- self.pager.updateState({ size: self.value.count });
+ }).then(() => {
+ this._updateControlPanel({ size: this.value.count });
});
}
var arch = this.view.arch;
@@ -1303,59 +1340,6 @@ var FieldX2Many = AbstractField.extend({
}
},
/**
- * Instanciates a control panel with the appropriate buttons and a pager.
- * Prepends the control panel's $el to this widget's $el.
- *
- * @private
- * @returns {Promise}
- */
- _renderControlPanel: function () {
- if (!this.view) {
- return Promise.resolve();
- }
- var self = this;
- var defs = [];
- var controlPanelView = new ControlPanelView({
- template: 'X2ManyControlPanel',
- withSearchBar: false,
- });
- var cpDef = controlPanelView.getController(this).then(function (controlPanel) {
- self._controlPanel = controlPanel;
- return self._controlPanel.prependTo(self.$el);
- });
- this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, {
- single_page_hidden: true,
- withAccessKey: false,
- validate: function () {
- var isList = self.view.arch.tag === 'tree';
- // TODO: we should have some common method in the basic renderer...
- return isList ? self.renderer.unselectRow() : Promise.resolve();
- },
- });
- this.pager.on('pager_changed', this, function (new_state) {
- self.trigger_up('load', {
- id: self.value.id,
- limit: new_state.limit,
- offset: new_state.current_min - 1,
- on_success: function (value) {
- self.value = value;
- self._render();
- },
- });
- });
- this._renderButtons();
- defs.push(this.pager.appendTo($('<div>'))); // start the pager
- defs.push(cpDef);
- return Promise.all(defs).then(function () {
- self._controlPanel.updateContents({
- cp_content: {
- $buttons: self.$buttons,
- $pager: self.pager.$el,
- }
- });
- });
- },
- /**
* Renders the buttons and sets this.$buttons.
*
* @private
@@ -1395,8 +1379,8 @@ var FieldX2Many = AbstractField.extend({
} else {
self.renderer.setRowMode(recordID, 'readonly').then(resolve);
}
- }).then(function () {
- self.pager.updateState({ size: self.value.count });
+ }).then(async function () {
+ self._updateControlPanel({ size: self.value.count });
var newEval = self._evalColumnInvisibleFields();
if (!_.isEqual(self.currentColInvisibleFields, newEval)) {
self.currentColInvisibleFields = newEval;
@@ -1409,18 +1393,17 @@ var FieldX2Many = AbstractField.extend({
/**
* Re-renders buttons and updates the control panel. This method is called
* when the widget is reset, as the available buttons might have changed.
+ * The only mutable element in X2Many fields will be the pager.
*
* @private
*/
- _updateControlPanel: function () {
- if (this._controlPanel) {
- this._renderButtons();
- const params = {
- cp_content: {
- $buttons: this.$buttons,
- }
+ _updateControlPanel: function (pagingState) {
+ if (this._controlPanelWrapper) {
+ const newProps = {
+ cp_content: { $buttons: this.$buttons },
+ pager: Object.assign(this.pagingState, pagingState),
};
- this._controlPanel.updateContents(params, { clear: false });
+ return this._controlPanelWrapper.update(newProps);
}
},
/**
@@ -1577,6 +1560,30 @@ var FieldX2Many = AbstractField.extend({
// to implement
},
/**
+ * We re-render the pager immediately with the new event values to allow
+ * it to request another pager change while another one is still ongoing.
+ * @see field_manager_mixin for concurrency handling.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onPagerChanged: function (ev) {
+ ev.stopPropagation();
+ const { currentMinimum, limit } = ev.data;
+ this._updateControlPanel({ currentMinimum, limit });
+ this.trigger_up('load', {
+ id: this.value.id,
+ limit,
+ offset: currentMinimum - 1,
+ on_success: value => {
+ this.value = value;
+ this.pagingState.limit = value.limit;
+ this.pagingState.size = value.count;
+ this._render();
+ },
+ });
+ },
+ /**
* Called when the renderer ask to save a line (the user tries to leave it)
* -> Nothing is to "save" here, the model was already notified of the line
* changes; if the row could be saved, we make the row readonly. Otherwise,
@@ -1777,7 +1784,7 @@ var FieldOne2Many = FieldX2Many.extend({
*/
reset: function (record, ev) {
var self = this;
- return this._super.apply(this, arguments).then(function () {
+ return this._super.apply(this, arguments).then(() => {
if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') {
if (ev.data.changes[self.name] && ev.data.changes[self.name].operation === 'CREATE') {
var index = 0;
@@ -1792,7 +1799,7 @@ var FieldOne2Many = FieldX2Many.extend({
// have 3 records, and we click on add, we will see the
// 4 records on the same page, but we do not want a
// pager.
- self.pager.updateState({ size: self.value.count - 1});
+ self._updateControlPanel({ size: self.value.count - 1 });
}
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
@@ -1832,7 +1839,7 @@ var FieldOne2Many = FieldX2Many.extend({
*/
_renderButtons: function () {
if (this.activeActions.create) {
- this._super.apply(this, arguments);
+ return this._super(...arguments);
}
},
/**
diff --git a/addons/web/static/src/js/report/client_action.js b/addons/web/static/src/js/report/client_action.js
index c8b5621e6de..9bd7c037d6e 100644
--- a/addons/web/static/src/js/report/client_action.js
+++ b/addons/web/static/src/js/report/client_action.js
@@ -37,7 +37,7 @@ var ReportAction = AbstractAction.extend({
start: function () {
var self = this;
this.iframe = this.$('iframe')[0];
- return Promise.all([this._super.apply(this, arguments), session.is_bound]).then(function () {
+ return Promise.all([this._super.apply(this, arguments), session.is_bound]).then(async function () {
var web_base_url = session['web.base.url'];
var trusted_host = utils.get_host_from_url(web_base_url);
var trusted_protocol = utils.get_protocol_from_url(web_base_url);
@@ -46,7 +46,11 @@ var ReportAction = AbstractAction.extend({
self.$buttons = $(QWeb.render('report.client_action.ControlButtons', {}));
self.$buttons.on('click', '.o_report_print', self.on_click_print);
- self._update_control_panel();
+ await self.updateControlPanel({
+ cp_content: {
+ $buttons: self.$buttons,
+ },
+ });
// Load the report in the iframe. Note that we use a relative URL.
self.iframe.src = self.report_url;
@@ -54,7 +58,11 @@ var ReportAction = AbstractAction.extend({
},
do_show: function () {
- this._update_control_panel();
+ this.updateControlPanel({
+ cp_content: {
+ $buttons: this.$buttons,
+ },
+ });
return this._super.apply(this, arguments);
},
@@ -69,14 +77,6 @@ var ReportAction = AbstractAction.extend({
$(window).off('message', this.on_message_received);
},
- _update_control_panel: function () {
- this.updateControlPanel({
- cp_content: {
- $buttons: this.$buttons,
- },
- });
- },
-
/**
* Event handler of the message post. We only handle them if they're from
* `web.base.url` host and protocol and if they're part of `AUTHORIZED_MESSAGES`.
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 45374d4833b..12d7ce24f8d 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -99,26 +99,28 @@ return core.Class.extend({
},
model: model,
method: 'load_views',
- }).then(function (result) {
+ }).then(async result => {
// Freeze the fields dict as it will be shared between views and
// no one should edit it
utils.deepFreeze(result.fields);
// Insert views into the fields_views cache
- _.each(views_descr, function (view_descr) {
+ for (const view_descr of views_descr) {
var toolbar = options.toolbar && view_descr[1] !== 'search';
var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
var fvg = result.fields_views[view_descr[1]];
+ if (view_descr[1] === 'search') {
+ fvg.favoriteFilters = result.filters;
+ }
fvg.viewFields = fvg.fields;
fvg.fields = result.fields;
self._cache.fields_views[fv_key] = Promise.resolve(fvg);
- });
+ }
// Insert filters, if any, into the filters cache
if (result.filters) {
self._cache.filters[filters_key] = Promise.resolve(result.filters);
}
-
return result.fields_views;
}, this._invalidate.bind(this, this._cache.views, key));
}
diff --git a/addons/web/static/src/js/tools/debug_manager_backend.js b/addons/web/static/src/js/tools/debug_manager_backend.js
index 2c445ff745e..b9f211872eb 100644
--- a/addons/web/static/src/js/tools/debug_manager_backend.js
+++ b/addons/web/static/src/js/tools/debug_manager_backend.js
@@ -245,7 +245,7 @@ DebugManager.include({
action: this._action,
can_edit: this._can_edit_views,
controller: this._controller,
- controlPanelView: this._controller && this._controller._controlPanel,
+ withControlPanel: this._controller && this._controller.withControlPanel,
manager: this,
view: this._controller && _.findWhere(this._action.views, {
type: this._controller.viewType,
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();
});
diff --git a/addons/web/static/src/js/widgets/pie_chart.js b/addons/web/static/src/js/widgets/pie_chart.js
index a9b0bc01e45..7cfc3fdacce 100644
--- a/addons/web/static/src/js/widgets/pie_chart.js
+++ b/addons/web/static/src/js/widgets/pie_chart.js
@@ -41,7 +41,9 @@ var PieChart = Widget.extend({
this.subViewParams = {
modelName: record.model,
+ withButtons: false,
withControlPanel: false,
+ withSearchPanel: false,
isEmbedded: true,
mode: 'pie',
};
@@ -49,6 +51,7 @@ var PieChart = Widget.extend({
context: pieChartContext,
domain: domain,
groupBy: [],
+ timeRanges: {},
};
this.viewInfo = {
diff --git a/addons/web/static/src/xml/debug.xml b/addons/web/static/src/xml/debug.xml
index 4a53168c84b..0caf3da010e 100644
--- a/addons/web/static/src/xml/debug.xml
+++ b/addons/web/static/src/xml/debug.xml
@@ -59,7 +59,7 @@
<a role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fieldsView.view_id" class="dropdown-item">
Edit View: <t t-esc="view.displayName"/>
</a>
- <a t-if="controlPanelView and controlPanelView.$el.is(':visible')" role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="action.controlPanelFieldsView.view_id" class="dropdown-item">Edit ControlPanelView</a>
+ <a t-if="withControlPanel" role="menuitem" href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="action.controlPanelFieldsView.view_id" class="dropdown-item">Edit ControlPanelView</a>
</t>
</t>
</t>