summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-20 17:33:22 +0200
committerChristophe Simonis <chs@odoo.com>2019-09-20 17:34:45 +0200
commit64e43808b79badb5869a8d73cc1cc3f3b3aa8e12 (patch)
tree9a22d5f6c03073a4d10faedea8df23a5211e8b8f /addons/web/static/src/js/views
parent944891ced081cda3b8b3db5faf3b7ac23b6b4ce8 (diff)
parent58a83d12228fbbd4e9dde542e133d264204ee0f3 (diff)
[MERGE] forward port branch saas-12.5 up to 58a83d12228
Diffstat (limited to 'addons/web/static/src/js/views')
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js3
-rw-r--r--addons/web/static/src/js/views/abstract_view.js18
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js6
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js2
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_renderer.js1
-rw-r--r--addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js31
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_bar.js20
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_facet.js20
-rw-r--r--addons/web/static/src/js/views/graph/graph_controller.js2
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js29
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js173
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js69
-rw-r--r--addons/web/static/src/js/views/list/list_model.js10
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js21
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js18
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js5
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js1
-rw-r--r--addons/web/static/src/js/views/search_panel.js17
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js5
19 files changed, 351 insertions, 100 deletions
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index cb83301c14d..43a485fb009 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -19,6 +19,8 @@ var config = require('web.config');
var core = require('web.core');
var mvc = require('web.mvc');
+var session = require('web.session');
+
var QWeb = core.qweb;
var AbstractController = mvc.Controller.extend(ActionMixin, {
@@ -519,6 +521,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
this.dp.add(this._rpc({
model: data.model,
method: data.method,
+ context: session.user_context,
})).then(function (action) {
if (action !== undefined) {
self.do_action(action, options);
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index d581f84308a..7dc3cc30563 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -91,8 +91,8 @@ var AbstractView = Factory.extend({
* @param {Array[]} [params.searchQuery.domain=[]]
* @param {string[]} [params.searchQuery.groupBy=[]]
* @param {Object} [params.userContext={}]
- * @param {boolean} [params.withControlPanel=true]
- * @param {boolean} [params.withSearchPanel=true]
+ * @param {boolean} [params.withControlPanel=AbstractView.prototype.withControlPanel]
+ * @param {boolean} [params.withSearchPanel=AbstractView.prototype.withSearchPanel]
*/
init: function (viewInfo, params) {
this._super.apply(this, arguments);
@@ -115,7 +115,9 @@ var AbstractView = Factory.extend({
this.fields = this.fieldsView.viewFields;
this.userContext = params.userContext || {};
this.withControlPanel = this.withControlPanel && params.withControlPanel;
- this.withSearchPanel = this.withSearchPanel && this.multi_record && params.withSearchPanel;
+ const searchPanelDisabled = 'search_panel' in params.context && !params.search_panel;
+ this.withSearchPanel = this.withSearchPanel && this.multi_record &&
+ params.withSearchPanel && !searchPanelDisabled;
// the boolean parameter 'isEmbedded' determines if the view should be
// considered as a subview. For now this is only used by the graph
@@ -187,6 +189,9 @@ var AbstractView = Factory.extend({
withSearchBar: params.withSearchBar,
};
this.searchPanelParams = {
+ defaultNoFilter: params.searchPanelDefaultNoFilter,
+ fields: this.fields,
+ model: this.loadParams.modelName,
state: controllerState.spState,
};
},
@@ -292,15 +297,12 @@ var AbstractView = Factory.extend({
}
});
var controlPanelDomain = this.loadParams.domain;
- var searchPanel = new this.config.SearchPanel(parent, {
+ var spParams = _.extend({}, this.searchPanelParams, {
defaultValues: defaultValues,
- fields: this.fields,
- model: this.loadParams.modelName,
searchDomain: controlPanelDomain,
- sections: this.searchPanelParams.sections,
- state: this.searchPanelParams.state,
classes: params.classes || [],
});
+ var searchPanel = new this.config.SearchPanel(parent, spParams);
this.controllerParams.searchPanel = searchPanel;
this.controllerParams.controlPanelDomain = controlPanelDomain;
await searchPanel.appendTo(document.createDocumentFragment());
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 2d239520dfb..4b4d02c2755 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1199,7 +1199,7 @@ var BasicModel = AbstractModel.extend({
if (!_.isEmpty(action)) {
return self.do_action(action, {
on_close: function () {
- return self.reload(parentID);
+ return self.trigger_up('reload');
}
});
} else {
@@ -1231,7 +1231,7 @@ var BasicModel = AbstractModel.extend({
if (!_.isEmpty(action)) {
return self.do_action(action, {
on_close: function () {
- return self.reload(parentID);
+ return self.trigger_up('reload');
}
});
} else {
@@ -1263,7 +1263,7 @@ var BasicModel = AbstractModel.extend({
if (!_.isEmpty(action)) {
return self.do_action(action, {
on_close: function () {
- return self.reload(parentID);
+ return self.trigger_up('reload');
}
});
} else {
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 232a7316fa5..1b081316f20 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -105,7 +105,7 @@ var CalendarView = AbstractView.extend({
};
if (fields[fieldName].relation) {
if (['res.users', 'res.partner', 'hr.employee'].indexOf(fields[fieldName].relation) !== -1) {
- filters[fieldName].avatar_field = 'image_64';
+ filters[fieldName].avatar_field = 'image_128';
}
filters[fieldName].avatar_model = fields[fieldName].relation;
}
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
index 677f0733bc5..bdde2827e96 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
@@ -50,6 +50,7 @@ var ControlPanelRenderer = Renderer.extend({
this.$subMenus = null;
this.action = params.action;
this.displaySearchMenu = true;
+ this.isMobile = config.device.isMobile;
this.menusSetup = false;
this.searchMenuTypes = params.searchMenuTypes || [];
this.subMenus = {};
diff --git a/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js
index 23852abcf53..95b5c577587 100644
--- a/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/mobile_control_panel_renderer.js
@@ -9,9 +9,9 @@ if (!config.device.isMobile) {
}
ControlPanelRenderer.include({
- template:'MobileControlPanel',
events:_.extend({}, ControlPanelRenderer.prototype.events, {
- 'click .o_mobile_search_close, .o_mobile_search_show_result, .o_enable_searchview': '_toggleMobileSearchView',
+ 'click .o_mobile_search_close, .o_mobile_search_show_result, .o_toggle_searchview_full': '_toggleMobileSearchView',
+ 'click .o_enable_searchview': '_toggleMobileQuickSearchView',
'click': '_onOpenMobileSearchView',
'click .o_mobile_search_clear_facets': '_onEmptyAll',
'show.bs.dropdown .o_mobile_search_filter .o_dropdown': '_onDropdownToggle',
@@ -49,17 +49,40 @@ ControlPanelRenderer.include({
_renderSearchBar: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
- self.searchBar.$el.detach().insertAfter(self.$('.o_mobile_search_header'));
+ self._renderSearchviewInput();
});
},
+ _renderSearchviewInput: function () {
+ if (this.$('.o_toggle_searchview_full').is(':visible') && !this.$('.o_mobile_search').is(':visible')) {
+ this.$('.o_toggle_searchview_full').toggleClass('btn-secondary', !!this.state.query.length);
+ this.searchBar.$el.detach().insertAfter(this.$('.o_mobile_search'));
+ } else {
+ this.searchBar.$el.detach().insertAfter(this.$('.o_mobile_search_header'));
+ }
+ },
/**
* Toggles mobile search view screen.
*
* @private
*/
_toggleMobileSearchView: function () {
- this.$('.o_enable_searchview').toggleClass('btn-secondary', !!this.state.query.length);
this.$('.o_mobile_search').toggleClass('o_hidden');
+ this._renderSearchviewInput();
+ },
+ /**
+ * Toggles mobile quick search view on screen.
+ *
+ * @private
+ */
+ _toggleMobileQuickSearchView: function () {
+ this.$('.o_cp_searchview').toggleClass('o_searchview_quick');
+ this.$('.breadcrumb').toggleClass('o_hidden',
+ this.$('.o_cp_searchview').hasClass('o_searchview_quick'));
+ this.$('.o_toggle_searchview_full')
+ .toggleClass('o_hidden')
+ .toggleClass('btn-secondary', !!this.state.query.length);
+ this._renderSearchviewInput();
+ this.$('.o_enable_searchview').toggleClass("fa-search").toggleClass("fa-close");
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/control_panel/search/search_bar.js b/addons/web/static/src/js/views/control_panel/search/search_bar.js
index 3f58fcdce2a..42b156beed7 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_bar.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_bar.js
@@ -9,6 +9,8 @@ var Widget = require('web.Widget');
var SearchBar = Widget.extend({
template: 'SearchView.SearchBar',
events: _.extend({}, Widget.prototype.events, {
+ 'compositionend .o_searchview_input': '_onCompositionendInput',
+ 'compositionstart .o_searchview_input': '_onCompositionstartInput',
'keydown': '_onKeydown',
}),
/**
@@ -32,6 +34,7 @@ var SearchBar = Widget.extend({
this.autoCompleteSources = [];
this.searchFacets = [];
+ this._isInputComposing = false;
},
/**
* @override
@@ -200,10 +203,27 @@ var SearchBar = Widget.extend({
}
},
/**
+ * @rivate
+ * @param {CompositionEvent} ev
+ */
+ _onCompositionendInput: function () {
+ this._isInputComposing = false;
+ },
+ /**
+ * @rivate
+ * @param {CompositionEvent} ev
+ */
+ _onCompositionstartInput: function () {
+ this._isInputComposing = true;
+ },
+ /**
* @private
* @param {KeyEvent} e
*/
_onKeydown: function (e) {
+ if (this._isInputComposing) {
+ return;
+ }
switch(e.which) {
case $.ui.keyCode.LEFT:
this._focusPreceding();
diff --git a/addons/web/static/src/js/views/control_panel/search/search_facet.js b/addons/web/static/src/js/views/control_panel/search/search_facet.js
index 917d42b7e41..22d957c030b 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_facet.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_facet.js
@@ -10,6 +10,8 @@ var SearchFacet = Widget.extend({
template: 'SearchView.SearchFacet',
events: _.extend({}, Widget.prototype.events, {
'click .o_facet_remove': '_onFacetRemove',
+ 'compositionend': '_onCompositionend',
+ 'compositionstart': '_onCompositionstart',
'keydown': '_onKeydown',
}),
/**
@@ -26,6 +28,7 @@ var SearchFacet = Widget.extend({
});
this.separator = this._getSeparator();
this.icon = this._getIcon();
+ this._isComposing = false;
},
//--------------------------------------------------------------------------
@@ -130,6 +133,20 @@ var SearchFacet = Widget.extend({
/**
* @private
+ * @param {CompositionEvent} ev
+ */
+ _onCompositionend: function (ev) {
+ this._isComposing = false;
+ },
+ /**
+ * @private
+ * @param {CompositionEvent} ev
+ */
+ _onCompositionstart: function (ev) {
+ this._isComposing = true;
+ },
+ /**
+ * @private
*/
_onFacetRemove: function () {
this.trigger_up('facet_removed', {group: this.facet});
@@ -139,6 +156,9 @@ var SearchFacet = Widget.extend({
* @param {KeyboardEvent} ev
*/
_onKeydown: function (ev) {
+ if (this._isComposing) {
+ return;
+ }
switch (ev.which) {
case $.ui.keyCode.BACKSPACE:
this.trigger_up('facet_removed', {group: this.facet});
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 2953b99f731..1c580a25bd6 100644
--- a/addons/web/static/src/js/views/graph/graph_controller.js
+++ b/addons/web/static/src/js/views/graph/graph_controller.js
@@ -177,7 +177,7 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
.find('.o_graph_button[data-mode="stack"]')
.data('stacked', state.stacked)
.toggleClass('active', state.stacked)
- .toggleClass('o_hidden', state.mode !== 'bar' || !this.renderer.isStackable);
+ .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);
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index e24de02879c..6cb2c023d0e 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -71,8 +71,6 @@ return AbstractRenderer.extend({
this.chart = null;
this.chartId = _.uniqueId('chart');
- // the bar charts are considered stackable a priori
- this.isStackable = true;
this.$legendTooltip = null;
this.$tooltip = null;
},
@@ -782,34 +780,9 @@ return AbstractRenderer.extend({
// prepare data
var data = this._prepareData(dataPoints);
- // The datapoints (with non zero value) can be grouped by
- // their originIndex and associated labels.
- // If at least one group formed in that way has two elements
- // this.isStackable will be true.
- var groupMemberCounts = data.labels.map(function () {
- return {};
- });
- this.isStackable = false;
- for (var i = 0; i < data.datasets.length; i++) {
- var dataset = data.datasets[i];
- var originIndex = dataset.originIndex;
- if (this.isStackable) {
- break;
- }
- for (var j = 0; j < dataset.data.length; j++) {
- if (dataset.data[j]) {
- if (originIndex in groupMemberCounts[j]) {
- this.isStackable = true;
- break;
- } else {
- groupMemberCounts[j][originIndex] = 1;
- }
- }
- }
- }
data.datasets.forEach(function (dataset, index) {
// used when stacked
- dataset.stack = self.isStackable && self.state.stacked ? self.state.origins[dataset.originIndex] : undefined;
+ dataset.stack = self.state.stacked ? self.state.origins[dataset.originIndex] : undefined;
// set dataset color
var color = self._getColor(index);
dataset.backgroundColor = color;
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 5ec42156000..4e0060fa5ff 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -8,20 +8,78 @@ odoo.define('web.ListController', function (require) {
*/
var core = require('web.core');
+var session = require('web.session');
+var framework = require('web.framework');
var BasicController = require('web.BasicController');
var DataExport = require('web.DataExport');
var Dialog = require('web.Dialog');
+var fieldUtils = require('web.field_utils');
var Sidebar = require('web.Sidebar');
var _t = core._t;
var qweb = core.qweb;
+/**
+ * Return data required for exporting the current list to a xls file.
+ * Recursive if some groups are nested.
+ * Note: this function is also used when the records are not grouped.
+ * In this scenario, an artificial single group with every record in
+ * the list is built.
+ * @param {Array} groups
+ * @param {Array} displayedFields
+ * @param {Array} allFields
+ * @returns {Array} list of groups with the following structure:
+ * 'data': list of records or list of sub-groups
+ * 'isGrouped': true if data contains sub-groups
+ * 'value': title of the group
+ * 'count': number of records in the group
+ * 'aggregateValues': dict container aggregated values of fields {field: aggregatedValue, ...}
+ * 'hideHeader': self explanatory, used when the records are not grouped at all.
+ */
+var processGroups = (groups, displayedFields, allFields) => {
+ let formatRecord = record => {
+ let data = _.pick(record.data, displayedFields)
+ let formattedData = {}
+
+ Object.entries(data).forEach(([field_name, value]) => {
+ let field = allFields[field_name]
+ let formattedValue = fieldUtils.format[field.type](value, field, {data: record.data, forceString: true})
+ formattedData[field_name] = formattedValue.replace('&nbsp;', '')
+ });
+ return formattedData
+ }
+
+ if (groups.length && groups[0].groupedBy && groups[0].groupedBy.length) {
+ // Recursively process sub-group
+ return groups.map(group => ({
+ hideHeader: !!group.hideHeader,
+ isGrouped: true,
+ value: group.value !== undefined && group.value.toString() || _t("Undefined"),
+ count: group.count,
+ aggregateValues: _.pick(group.aggregateValues, displayedFields),
+ data: processGroups(group.data, displayedFields, allFields),
+ }))
+ }
+ // process records
+ return groups.map(group => ({
+ hideHeader: !!group.hideHeader,
+ isGrouped: false,
+ value: group.value !== undefined && group.value.toString() || _t("Undefined"),
+ count: group.count,
+ aggregateValues: _.pick(group.aggregateValues, displayedFields),
+ data: group.data.map(formatRecord)
+ }))
+}
+
var ListController = BasicController.extend({
/**
* This key contains the name of the buttons template to render on top of
* the list view. It can be overridden to add buttons in specific child views.
*/
buttons_template: 'ListView.buttons',
+ events: _.extend({}, BasicController.prototype.events, {
+ 'click .o_list_download': '_onExportRecords',
+ }),
custom_events: _.extend({}, BasicController.prototype.custom_events, {
activate_next_widget: '_onActivateNextWidget',
add_record: '_onAddRecord',
@@ -50,6 +108,10 @@ var ListController = BasicController.extend({
this.noLeaf = params.noLeaf;
this.selectedRecords = params.selectedRecords || [];
this.multipleRecordsSavingPromise = null;
+ this.fieldChangedPrevented = false;
+ this.activeActions = _.extend({}, this.activeActions, {
+ download: this.renderer.arch.attrs.download ? !!JSON.parse(this.renderer.arch.attrs.download) : true,
+ });
},
//--------------------------------------------------------------------------
@@ -134,6 +196,7 @@ var ListController = BasicController.extend({
},
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);
}
@@ -339,6 +402,31 @@ var ListController = BasicController.extend({
});
},
/**
+ * Export the current list data in a xls file.
+ *
+ * @private
+ */
+ _downloadList() {
+ let groups = this.renderer.state.data
+ groups = this.renderer.isGrouped ? groups : [{data: groups, count: groups.length, hideHeader: true}] // Artificial single group
+
+ let allFields = this.renderer.state.fields
+ let columns = this.renderer.columns.map(column => ({
+ field: column.attrs.name,
+ aggregateValue: column.aggregate && column.aggregate.value,
+ string: allFields[column.attrs.name].string,
+ }))
+
+ groups = processGroups(groups, columns.map(c => c.field), allFields)
+
+ return session.get_file({
+ url: '/web/list/export_xls',
+ data: {data: JSON.stringify({columns, groups, title: this._title})},
+ complete: framework.unblockUI,
+ error: (error) => this.call('crash_manager', 'rpc_error', error),
+ });
+ },
+ /**
* @override
* @private
*/
@@ -371,6 +459,7 @@ var ListController = BasicController.extend({
*/
_saveMultipleRecords: function (recordId, node, changes) {
var self = this;
+ var fieldName = Object.keys(changes)[0];
var value = Object.values(changes)[0];
var recordIds = _.union([recordId], this.selectedRecords);
var validRecordIds = recordIds.reduce(function (result, nextRecordId) {
@@ -388,6 +477,7 @@ var ListController = BasicController.extend({
this.model.discardChanges(recordId);
return this._confirmSave(recordId).then(reject);
};
+ let dialog;
if (validRecordIds.length > 0) {
let message;
if (nbInvalid === 0) {
@@ -399,13 +489,13 @@ var ListController = BasicController.extend({
_t("Do you want to set the value on the %d valid selected records? (%d invalid)"),
validRecordIds.length, nbInvalid);
}
- Dialog.confirm(this, message, {
+ dialog = Dialog.confirm(this, message, {
confirm_callback: () => {
- this.model.saveRecords(recordId, validRecordIds)
- .then(() => {
+ return this.model.saveRecords(recordId, validRecordIds, fieldName)
+ .then(async () => {
this._updateButtons('readonly');
const state = this.model.get(this.handle);
- this.renderer.updateState(state, {keepWidths: true});
+ await this.renderer.updateState(state, {keepWidths: true});
resolve(Object.keys(changes));
})
.guardedCatch(rejectAndDiscard);
@@ -413,10 +503,17 @@ var ListController = BasicController.extend({
cancel_callback: rejectAndDiscard,
});
} else {
- Dialog.alert(this, _t("No valid record to save"), {
+ dialog = Dialog.alert(this, _t("No valid record to save"), {
confirm_callback: rejectAndDiscard,
});
}
+ dialog.on('closed', this, async () => {
+ // we need to wait for the dialog to be actually closed, but
+ // the 'closed' event is triggered just before, and it prevents
+ // from focussing the cell
+ await Promise.resolve();
+ this.renderer.focusCell(recordId, node);
+ });
});
},
/**
@@ -587,6 +684,28 @@ var ListController = BasicController.extend({
this._discardChanges();
},
/**
+ * Used to detect if the discard button is about to be clicked.
+ * Some focusout events might occur and trigger a save which
+ * is not always wanted when clicking "Discard".
+ *
+ * @param {MouseEvent} ev
+ * @private
+ */
+ _onDiscardMousedown: function (ev) {
+ var self = this;
+ this.fieldChangedPrevented = true;
+ window.addEventListener('mouseup', function (mouseupEvent) {
+ var preventedEvent = self.fieldChangedPrevented;
+ self.fieldChangedPrevented = false;
+ // If the user starts clicking (mousedown) on the button and stops clicking
+ // (mouseup) outside of the button, we want to trigger the original onFieldChanged
+ // Event that was prevented in the meantime.
+ if (ev.target !== mouseupEvent.target && preventedEvent.constructor.name === 'OdooEvent') {
+ self._onFieldChanged(preventedEvent);
+ }
+ }, { capture: true, once: true });
+ },
+ /**
* Called when the user asks to edit a row -> Updates the controller buttons
*
* @param {OdooEvent} ev
@@ -614,6 +733,14 @@ var ListController = BasicController.extend({
new DataExport(this, record, defaultExportFields).open();
},
/**
+ * Export Records in a xls file
+ *
+ * @private
+ */
+ _onExportRecords() {
+ this._downloadList()
+ },
+ /**
* Opens the related form view.
*
* @private
@@ -643,15 +770,16 @@ var ListController = BasicController.extend({
ev.stopPropagation();
const recordId = ev.data.dataPointID;
- if (this.renderer.inMultipleRecordEdition(recordId)) {
+ if (this.fieldChangedPrevented) {
+ this.fieldChangedPrevented = ev;
+ } else if (this.renderer.inMultipleRecordEdition(recordId)) {
// deal with edition of multiple lines
- const _onSuccess = ev.data.onSuccess;
ev.data.onSuccess = () => {
- Promise.resolve(_onSuccess()).then(() => {
- const savedRecordsPromise = this._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes);
- this.multipleRecordsSavingPromise = savedRecordsPromise;
- });
+ this.multipleRecordsSavingPromise =
+ this._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes);
};
+ // disable onchanges as we'll save directly
+ ev.data.notifyChange = false;
}
this._super.apply(this, arguments);
},
@@ -678,6 +806,29 @@ var ListController = BasicController.extend({
this._toggleSidebar();
},
/**
+ * If the record is set as dirty while in multiple record edition,
+ * we want to immediatly discard the change.
+ *
+ * @private
+ * @override
+ * @param {OdooEvent} ev
+ */
+ _onSetDirty: function (ev) {
+ var self = this;
+ var recordId = ev.data.dataPointID;
+ if (this.renderer.inMultipleRecordEdition(recordId)) {
+ ev.stopPropagation();
+ Dialog.alert(this, _t("No valid record to save"), {
+ confirm_callback: function () {
+ self.model.discardChanges(recordId);
+ self._confirmSave(recordId);
+ },
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
* When the user clicks on one of the sortable column headers, we need to
* tell the model to sort itself properly, to update the pager and to
* rerender the view.
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index d9cc1115917..6f21653b70c 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -19,6 +19,7 @@ var utils = require('web.utils');
var _t = core._t;
ListRenderer.include({
+ RESIZE_DELAY: 200,
custom_events: _.extend({}, ListRenderer.prototype.custom_events, {
navigation_move: '_onNavigationMove',
}),
@@ -109,6 +110,7 @@ ListRenderer.include({
start: function () {
if (this.editable) {
core.bus.on('click', this, this._onWindowClicked.bind(this));
+ core.bus.on('resize', this, _.debounce(this._onResize.bind(this), this.RESIZE_DELAY));
}
return this._super();
},
@@ -320,6 +322,17 @@ ListRenderer.include({
this._selectCell(rowIndex, 0);
},
/**
+ * Gives focus to a specific cell, given its row and its related column.
+ *
+ * @param {string} recordId
+ * @param {Object} column
+ */
+ focusCell: function (recordId, column) {
+ var $row = this._getRow(recordId);
+ var cellIndex = this.columns.indexOf(column);
+ $row.find('.o_data_cell')[cellIndex].focus();
+ },
+ /**
* Returns the recordID associated to the line which is currently in edition
* or null if there is no line in edition.
*
@@ -339,9 +352,7 @@ ListRenderer.include({
* @returns {boolean}
*/
inMultipleRecordEdition: function (recordId) {
- const record = this._getRecord(recordId) || {};
- const recordIds = [...new Set([recordId, ...this.selection])];
- return this.editable && recordIds.length > 1 && record.res_id;
+ return this.editable && this.selection.length > 1 && this.selection.includes(recordId);
},
/**
* Removes the line associated to the given recordID (the index of the row
@@ -358,6 +369,7 @@ ListRenderer.include({
}
if ($row.prop('rowIndex') - 1 === this.currentRow) {
this.currentRow = null;
+ this._enableRecordSelectors();
}
// destroy widgets first
@@ -452,7 +464,11 @@ ListRenderer.include({
// Toggle selected class here so that style is applied at the end
$row.toggleClass('o_selected_row', editMode);
- $row.find('.o_list_record_selector input').prop('disabled', !record.res_id);
+ if (editMode) {
+ this._disableRecordSelectors();
+ } else {
+ this._enableRecordSelectors();
+ }
return Promise.all(defs).then(function () {
// necessary to trigger resize on fieldtexts
@@ -494,6 +510,7 @@ ListRenderer.include({
onFailure: reject,
});
}).then(changedFields => {
+ this._enableRecordSelectors();
// If any field has changed and if the list is in multiple edition,
// we send a truthy boolean to _selectRow to tell it not to select
// the following record.
@@ -543,7 +560,14 @@ ListRenderer.include({
el.addEventListener(type, callback, options);
this.eventListeners.push({ type, el, callback, options });
},
-
+ /**
+ * When editing a row, we want to disable all record selectors.
+ *
+ * @private
+ */
+ _disableRecordSelectors: function () {
+ this.$('.o_list_record_selector input').attr('disabled', 'disabled');
+ },
/**
* Destroy all field widgets corresponding to a record. Useful when we are
* removing a useless row.
@@ -558,6 +582,12 @@ ListRenderer.include({
}
},
/**
+ * @private
+ */
+ _enableRecordSelectors: function () {
+ this.$('.o_list_record_selector input').attr('disabled', false);
+ },
+ /**
* This function freezes the column widths and forces a fixed table-layout,
* once the browser has computed the optimal width of each column according
* to the displayed records. We want to freeze widths s.t. it doesn't
@@ -581,9 +611,18 @@ ListRenderer.include({
const table = this.el.getElementsByTagName('table')[0];
const thead = table.getElementsByTagName('thead')[0];
+ // Set table layout auto and remove inline style to make sure that css
+ // rules apply (e.g. fixed width of record selector)
+ table.style.tableLayout = 'auto';
+ [...table.getElementsByTagName('th')].forEach((th) => {
+ th.style.width = null;
+ });
+
// Freeze each th width according to their size in auto layout
- [...thead.getElementsByTagName('th')].forEach((th, index) => {
- th.style.width = `${this.columnWidths ? this.columnWidths[index] : th.offsetWidth}px`;
+ const thElements = [...thead.getElementsByTagName('th')];
+ const thWidths = thElements.map((th) => th.offsetWidth);
+ thElements.forEach((th, index) => {
+ th.style.width = `${this.columnWidths ? this.columnWidths[index] : thWidths[index]}px`;
});
// Set the table layout to fixed
@@ -639,6 +678,7 @@ ListRenderer.include({
return fixedWidths[type] || '1';
},
/**
+ * @private
*
* @returns {integer}
*/
@@ -1195,7 +1235,10 @@ ListRenderer.include({
return new Promise(function (resolve) {
self.trigger_up('edit_line', {
recordId: recordId,
- onSuccess: resolve,
+ onSuccess: function () {
+ self._disableRecordSelectors();
+ resolve();
+ },
});
});
});
@@ -1386,6 +1429,7 @@ ListRenderer.include({
this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID,
onSuccess: function () {
+ self._enableRecordSelectors();
var recordId = self._getRecordID(rowIndex);
if (recordId) {
var correspondingRow = self._getRow(recordId);
@@ -1420,6 +1464,15 @@ ListRenderer.include({
}
},
/**
+ * React to window resize events by recomputing the width of each column.
+ *
+ * @private
+ */
+ _onResize: function () {
+ this.columnWidths = false;
+ this._freezeColumnWidths();
+ },
+ /**
* If the list view editable, just let the event bubble. We don't want to
* open the record in this case anyway.
*
diff --git a/addons/web/static/src/js/views/list/list_model.js b/addons/web/static/src/js/views/list/list_model.js
index 900f3764b0e..65526346c8e 100644
--- a/addons/web/static/src/js/views/list/list_model.js
+++ b/addons/web/static/src/js/views/list/list_model.js
@@ -40,12 +40,18 @@ odoo.define('web.ListModel', function (require) {
* @param {string} referenceRecordId the record datapoint used to
* generate the changes to apply to recordIds
* @param {string[]} recordIds a list of record datapoint ids
+ * @param {string} fieldName the field to write
*/
- saveRecords: function (referenceRecordId, recordIds) {
+ saveRecords: function (referenceRecordId, recordIds, fieldName) {
var self = this;
var referenceRecord = this.localData[referenceRecordId];
var list = this.localData[referenceRecord.parentID];
- var changes = this._generateChanges(referenceRecord, {});
+ // generate all record values to ensure that we'll write something
+ // (e.g. 2 records selected, edit a many2one in the first one, but
+ // reset same value, we still want to save this value on the other
+ // record)
+ var allChanges = this._generateChanges(referenceRecord, {changesOnly: false});
+ var changes = _.pick(allChanges, fieldName);
var records = recordIds.map(function (recordId) {
return self.localData[recordId];
});
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 94609544b10..5daa33125a7 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -996,11 +996,18 @@ var ListRenderer = BasicRenderer.extend({
* @private
*/
_updateSelection: function () {
- var $selectedRows = this.$('tbody .o_list_record_selector input:checked')
- .closest('tr');
- this.selection = _.map($selectedRows, function (row) {
- return $(row).data('id');
+ this.selection = [];
+ var self = this;
+ var $inputs = this.$('tbody .o_list_record_selector input:visible:not(:disabled)');
+ var allChecked = $inputs.length > 0;
+ $inputs.each(function (index, input) {
+ if (input.checked) {
+ self.selection.push($(input).closest('tr').data('id'));
+ } else {
+ allChecked = false;
+ }
});
+ this.$('thead .o_list_record_selector input').prop('checked', allChecked);
this.trigger_up('selection_changed', { selection: this.selection });
this._updateFooter();
},
@@ -1186,9 +1193,6 @@ var ListRenderer = BasicRenderer.extend({
_onSelectRecord: function (ev) {
ev.stopPropagation();
this._updateSelection();
- if (!$(ev.currentTarget).find('input').prop('checked')) {
- this.$('thead .o_list_record_selector input').prop('checked', false);
- }
},
/**
* @private
@@ -1209,7 +1213,8 @@ var ListRenderer = BasicRenderer.extend({
if (group.count) {
this.trigger_up('toggle_group', {
group: group,
- onSuccess: function () {
+ onSuccess: () => {
+ this._updateSelection();
// Refocus the header after re-render unless the user
// already focused something else by now
if (document.activeElement.tagName === 'BODY') {
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 47eb5ad49fe..733a812d328 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -13,7 +13,6 @@ odoo.define('web.PivotController', function (require) {
*/
var AbstractController = require('web.AbstractController');
-var config = require('web.config');
var core = require('web.core');
var framework = require('web.framework');
var session = require('web.session');
@@ -115,16 +114,6 @@ var PivotController = AbstractController.extend({
this._updateButtons();
}
},
- /**
- * @override
- * @param {Object} params
- */
- update: function (params) {
- if (config.device.isMobile && params.context) {
- params.context.pivot_column_groupby = [];
- }
- return this._super.apply(this, arguments);
- },
//--------------------------------------------------------------------------
// Private
@@ -218,12 +207,7 @@ var PivotController = AbstractController.extend({
.toggleClass('selected', isSelected);
});
var noDataDisplayed = !state.hasData || !state.measures.length;
- var $flipButton = this.$buttons.find('.o_pivot_flip_button');
- if (config.device.isMobile) {
- $flipButton.toggleClass('o_hidden', true);
- } else {
- $flipButton.prop('disabled', noDataDisplayed);
- }
+ 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);
},
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index b74b408c877..3affc975225 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -125,10 +125,7 @@ var PivotRenderer = AbstractRenderer.extend({
if (rowIndex > 0) {
cellParams.title = groupbyLabels[rowIndex - 1];
}
- className = 'o_pivot_header_cell';
- if (!config.device.isMobile) {
- className += cell.isLeaf ? '_closed' : '_opened';
- }
+ className = 'o_pivot_header_cell' + (cell.isLeaf ? '_closed' : '_opened');
}
cellParams.class = className;
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 78e7c68cbe6..1b878cf3530 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -108,7 +108,6 @@ var PivotView = AbstractView.extend({
this.loadParams.default_order = params.default_order || this.arch.attrs.default_order;
if (config.device.isMobile) {
this.loadParams.colGroupBys = [];
- this.loadParams.context.pivot_column_groupby = [];
}
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js
index 32bfda12594..4f8c42325db 100644
--- a/addons/web/static/src/js/views/search_panel.js
+++ b/addons/web/static/src/js/views/search_panel.js
@@ -77,6 +77,9 @@ var SearchPanel = Widget.extend({
* @param {Object} params
* @param {Object} [params.defaultValues={}] the value(s) to activate by
* default, for each filter and category
+ * @param {boolean} [params.defaultNoFilter=false] if true, select 'All' as
+ * value for each category that has no value specified in defaultValues
+ * (instead of looking in the localStorage for the last selected value)
* @param {Object} params.fields
* @param {string} params.model
* @param {Array[]} params.searchDomain domain coming from controlPanel
@@ -97,6 +100,12 @@ var SearchPanel = Widget.extend({
this.initialState = params.state;
this.scrollTop = this.initialState && this.initialState.scrollTop || null;
this.defaultValues = params.defaultValues || {};
+ if (params.defaultNoFilter) {
+ Object.keys(this.categories).forEach((categoryId) => {
+ var fieldName = this.categories[categoryId].fieldName;
+ this.defaultValues[fieldName] = this.defaultValues[fieldName] || false;
+ });
+ }
this.fields = params.fields;
this.model = params.model;
this.className = params.classes.concat(['o_search_panel']).join(' ');
@@ -181,7 +190,7 @@ var SearchPanel = Widget.extend({
return {
sections: searchPanelSections,
classes: classes,
- }
+ };
},
/**
* Export the current state (categories and filters) of the searchpanel.
@@ -279,8 +288,9 @@ var SearchPanel = Widget.extend({
});
_.map(values, function (value) {
var value = category.values[value.id];
- if (value.parentId) {
- category.values[value.parentId].childrenIds.push(value.id);
+ var parentCategoryId = value.parentId;
+ if (parentCategoryId && parentCategoryId in category.values) {
+ category.values[parentCategoryId].childrenIds.push(value.id);
}
});
category.rootIds = _.filter(_.map(values, function (value) {
@@ -293,7 +303,6 @@ var SearchPanel = Widget.extend({
// set active value
var validValues = _.pluck(category.values, 'id').concat([false]);
var value = this._getCategoryDefaultValue(category, validValues);
- // if not set in localStorage either, select 'All'
category.activeValueId = _.contains(validValues, value) ? value : false;
// unfold ancestor values of active value to make it is visible
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index 130173a16fc..47e3a5309d9 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -320,6 +320,7 @@ var SelectCreateDialog = ViewDialog.extend({
this._super.apply(this, arguments);
_.defaults(this.options, { initial_view: 'search' });
this.on_selected = this.options.on_selected || (function () {});
+ this.on_closed = this.options.on_closed || (function () {});
this.initialIDs = this.options.initial_ids;
this.viewType = 'list';
},
@@ -395,6 +396,10 @@ var SelectCreateDialog = ViewDialog.extend({
return fragment;
});
},
+ close: function () {
+ this._super.apply(this, arguments);
+ this.on_closed();
+ },
create_edit_record: function () {
var self = this;
var dialog = new FormViewDialog(this, _.extend({}, this.options, {