summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-23 17:40:06 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-24 09:44:56 +0000
commitac4fc6e7c7d71c3f8e2440a8ed8095c5aab15dd8 (patch)
treee143d6d261f22cb06f2c0d5b344a4353e5b3be1b /addons/web/static/src
parenta6a3df3d0eda0fc2476c59de61d59534815f7c20 (diff)
parentbd3e510edbcd62f508dae28f573ee4d1a51bcf87 (diff)
[MERGE] forward port branch saas-12.3 up to bd3e510edbc
closes odoo/odoo#37314 Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/css/reset.min.css4
-rw-r--r--addons/web/static/src/js/core/dom.js2
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js6
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js5
-rw-r--r--addons/web/static/src/js/fields/field_utils.js4
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js92
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js160
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js72
-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/view_dialogs.js5
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js9
-rw-r--r--addons/web/static/src/xml/base.xml4
13 files changed, 333 insertions, 61 deletions
diff --git a/addons/web/static/src/css/reset.min.css b/addons/web/static/src/css/reset.min.css
index 97c0b78b002..b6163764a5c 100644
--- a/addons/web/static/src/css/reset.min.css
+++ b/addons/web/static/src/css/reset.min.css
@@ -55,10 +55,6 @@ ul ul {
list-style: circle;
}
-blockquote, q {
- quotes: none;
-}
-
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 7be17700f98..e369cea9c8c 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -540,7 +540,7 @@ var dom = {
menuItemsWidth += computeFloatOuterWidthWithMargins($extraItemsToggle[0]);
do {
menuItemsWidth -= computeFloatOuterWidthWithMargins($items.eq(--nbItems)[0]);
- } while (!(maxWidth - menuItemsWidth >= -0.001));
+ } while (!(maxWidth - menuItemsWidth >= -0.001) && (nbItems > 0));
var $extraItems = $items.slice(nbItems).detach();
$extraItems.removeClass('nav-item');
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index dc456d21ae0..317ea30e0ad 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -533,6 +533,12 @@ var AbstractField = Widget.extend({
}
break;
case $.ui.keyCode.ENTER:
+ // We preventDefault the ENTER key because of two coexisting behaviours:
+ // - In HTML5, pressing ENTER on a <button> triggers two events: a 'keydown' AND a 'click'
+ // - When creating and opening a dialog, the focus is automatically given to the primary button
+ // The end result caused some issues where a modal opened by an ENTER keypress (e.g. saving
+ // changes in multiple edition) confirmed the modal without any intentionnal user input.
+ ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'next_line'});
break;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index eeda3d220b6..93a636c141a 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -492,6 +492,9 @@ var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
supportedFieldTypes: ['date', 'datetime'],
+ // we don't need to listen on 'input' nor 'change' events because the
+ // datepicker widget is already listening, and will correctly notify changes
+ events: AbstractField.prototype.events,
/**
* @override
@@ -888,6 +891,8 @@ var FieldBoolean = AbstractField.extend({
_onKeydown: function (ev) {
switch (ev.which) {
case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
this.$input.prop('checked', !this.value);
this._setValue(!this.value);
return;
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index ec99f4fa0f6..fd73b7a9ed2 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -415,7 +415,7 @@ function parseDate(value, field, options) {
if (options && options.isUTC) {
date = moment.utc(value);
} else {
- date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
+ date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601]);
}
if (date.isValid()) {
if (date.year() === 0) {
@@ -459,7 +459,7 @@ function parseDateTime(value, field, options) {
// phatomjs crash if we don't use this format
datetime = moment.utc(value.replace(' ', 'T') + 'Z');
} else {
- datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
+ datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601]);
if (options && options.timezone) {
datetime.add(-session.getTZOffset(datetime), 'minutes');
}
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index be35395aab7..6213fc8de6a 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -395,8 +395,10 @@ var FieldMany2One = AbstractField.extend({
no_create: !self.can_create,
on_selected: function (records) {
self.reinitialize(records[0]);
+ },
+ on_closed: function () {
self.activate();
- }
+ },
};
},
/**
@@ -805,6 +807,48 @@ var Many2oneBarcode = FieldMany2One.extend({
});
var ListFieldMany2One = FieldMany2One.extend({
+ events: _.extend({}, FieldMany2One.prototype.events, {
+ 'focusin input': '_onInputFocusin',
+ }),
+
+ /**
+ * Should never be allowed to be opened while in readonly mode in a list
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ // when we empty the input, we delay the setValue to prevent from
+ // triggering the 'fieldChanged' event twice when the user wants set
+ // another m2o value ; the following attribute is used to determine when
+ // we skipped the setValue, s.t. we can perform it later on if the user
+ // didn't select another value
+ this.mustSetValue = false;
+ this.m2oDialogFocused = false;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * If in readonly, will never be considered as an active widget.
+ *
+ * @override
+ */
+ activate: function () {
+ if (this.mode === 'readonly') {
+ return false;
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ reinitialize: function () {
+ this.mustSetValue = false;
+ return this._super.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Private
@@ -816,6 +860,52 @@ var ListFieldMany2One = FieldMany2One.extend({
_renderReadonly: function () {
this.$el.text(this.m2o_value);
},
+ /**
+ * @override
+ * @private
+ */
+ _searchCreatePopup: function () {
+ this.m2oDialogFocused = true;
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onInputFocusin: function () {
+ this.m2oDialogFocused = false;
+ },
+ /**
+ * In list views, we don't want to try to trigger a fieldChange when the field
+ * is being emptied. Instead, it will be triggered as the user leaves the field
+ * while it is empty.
+ *
+ * @override
+ * @private
+ */
+ _onInputFocusout: function () {
+ this._super.apply(this, arguments);
+ if (!this.m2oDialogFocused && this.$input.val() === "" && this.mustSetValue) {
+ this.reinitialize(false);
+ }
+ },
+ /**
+ * Prevents the triggering of an immediate _onFieldChanged when emptying the field.
+ *
+ * @override
+ * @private
+ */
+ _onInputKeyup: function () {
+ if (this.$input.val() !== "") {
+ this._super.apply(this, arguments);
+ } else {
+ this.mustSetValue = true;
+ }
+ },
});
var KanbanFieldMany2One = AbstractField.extend({
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 b864063ee92..e0598fcf9cc 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -50,6 +50,21 @@ var ListController = BasicController.extend({
this.editable = params.editable;
this.noLeaf = params.noLeaf;
this.selectedRecords = params.selectedRecords || [];
+ this.multipleRecordsSavingPromise = null;
+ this.fieldChangedPrevented = false;
+ this._onMouseupWindowDiscard = null;
+ },
+
+ /**
+ * Overriden to properly unbind any mouseup listeners still bound to the window
+ *
+ * @override
+ */
+ destroy: function () {
+ if (this._onMouseupWindowDiscard) {
+ window.removeEventListener('mouseup', this._onMouseupWindowDiscard, true);
+ }
+ return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
@@ -134,6 +149,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);
}
@@ -342,15 +358,6 @@ var ListController = BasicController.extend({
return _.extend(env, {domain: record.getDomain()});
},
/**
- * @private
- * @returns {boolean}
- */
- _inMultipleRecordEdition: function (recordId) {
- var record = this.model.get(recordId, { raw: true });
- var recordIds = _.union([recordId], this.selectedRecords);
- return recordIds.length > 1 && record.res_id;
- },
- /**
* Only display the pager when there are data to display.
*
* @override
@@ -361,13 +368,20 @@ var ListController = BasicController.extend({
return !!state.count;
},
/**
+ * Saves multiple records at once. This method is called by the _onFieldChanged method
+ * since the record must be confirmed as soon as the focus leaves a dirty cell.
+ * Pseudo-validation is performed with registered modifiers.
+ * Returns a promise that is resolved when confirming and rejected in any other case.
+ *
* @private
* @param {string} recordId
* @param {Object} node
* @param {Object} changes
+ * @returns {Promise}
*/
_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, recordId) {
@@ -378,22 +392,52 @@ var ListController = BasicController.extend({
}
return result;
}, []);
- var message = _.str.sprintf(
- _t('Do you want to set the value on the %d valid selected records?'),
- validRecordIds.length);
- if (recordIds.length !== validRecordIds.length) {
- var nbInvalid = recordIds.length - validRecordIds.length;
- message += ' ' + _.str.sprintf(_t('(%d invalid)'), nbInvalid);
- }
- Dialog.confirm(this, message, {
- confirm_callback: function () {
- self.model.saveRecords(recordId, validRecordIds)
- .then(function () {
- self._updateButtons('readonly');
- var state = self.model.get(self.handle);
- self.renderer.updateState(state, {});
- });
- },
+ var nbInvalid = recordIds.length - validRecordIds.length;
+
+ return new Promise(function (resolve, reject) {
+ var dialog;
+ var rejectAndDiscard = function () {
+ self.model.discardChanges(recordId);
+ return self._confirmSave(recordId).then(reject);
+ };
+ if (validRecordIds.length > 0) {
+ var message;
+ if (nbInvalid === 0) {
+ message = _.str.sprintf(
+ _t("Do you want to set the value on the %d selected records?"),
+ validRecordIds.length);
+ } else {
+ message = _.str.sprintf(
+ _t("Do you want to set the value on the %d valid selected records? (%d invalid)"),
+ validRecordIds.length, nbInvalid);
+ }
+ dialog = Dialog.confirm(self, message, {
+ confirm_callback: function () {
+ return self.model.saveRecords(recordId, validRecordIds, fieldName)
+ .then(function () {
+ self._updateButtons('readonly');
+ var state = self.model.get(self.handle);
+ return self.renderer.updateState(state, {}).then(function () {
+ resolve(Object.keys(changes));
+ });
+ })
+ .guardedCatch(rejectAndDiscard);
+ },
+ cancel_callback: rejectAndDiscard,
+ });
+ } else {
+ dialog = Dialog.alert(self, _t("No valid record to save"), {
+ confirm_callback: rejectAndDiscard,
+ });
+ }
+ dialog.on('closed', self, function () {
+ // 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
+ Promise.resolve().then(function () {
+ self.renderer.focusCell(recordId, node);
+ });
+ });
});
},
/**
@@ -404,10 +448,11 @@ var ListController = BasicController.extend({
*/
_saveRecord: function (recordId) {
var record = this.model.get(recordId, { raw: true });
- if (record.isDirty() && this._inMultipleRecordEdition(recordId)) {
+ if (record.isDirty() && this.renderer.inMultipleRecordEdition(recordId)) {
// do not save the record (see _saveMultipleRecords)
- return Promise.resolve();
-
+ var prom = this.multipleRecordsSavingPromise || Promise.reject();
+ this.multipleRecordsSavingPromise = null;
+ return prom;
}
return this._super.apply(this, arguments);
},
@@ -563,6 +608,31 @@ 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;
+ this._onMouseupWindowDiscard = (function (mouseupEvent) {
+ window.removeEventListener('mouseup', self._onMouseupWindowDiscard, true);
+ self._onMouseupWindowDiscard = null;
+ 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);
+ }
+ }).bind(this);
+ window.addEventListener('mouseup', this._onMouseupWindowDiscard, true);
+ },
+ /**
* Called when the user asks to edit a row -> Updates the controller buttons
*
* @param {OdooEvent} ev
@@ -618,15 +688,18 @@ var ListController = BasicController.extend({
_onFieldChanged: function (ev) {
ev.stopPropagation();
var self = this;
+ var recordId = ev.data.dataPointID;
- if (this._inMultipleRecordEdition(ev.data.dataPointID)) {
+ if (this.fieldChangedPrevented) {
+ this.fieldChangedPrevented = ev;
+ } else if (this.renderer.inMultipleRecordEdition(recordId)) {
// deal with edition of multiple lines
- var _onSuccess = ev.data.onSuccess;
ev.data.onSuccess = function () {
- Promise.resolve(_onSuccess()).then(function () {
+ self.multipleRecordsSavingPromise =
self._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);
},
@@ -653,6 +726,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 f87268ddb1e..4d5fd24124e 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
@@ -277,6 +277,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.
*
@@ -289,6 +300,16 @@ ListRenderer.include({
return null;
},
/**
+ * Returns whether the list is in multiple record edition from a given record.
+ *
+ * @private
+ * @param {string} recordId
+ * @returns {boolean}
+ */
+ inMultipleRecordEdition: function (recordId) {
+ return this.editable && this.selection.length > 1 && this.selection.indexOf(recordId) > -1;
+ },
+ /**
* Removes the line associated to the given recordID (the index of the row
* is found thanks to the old state), then updates the state.
*
@@ -303,6 +324,7 @@ ListRenderer.include({
}
if ($row.prop('rowIndex') - 1 === this.currentRow) {
this.currentRow = null;
+ this._enableRecordSelectors();
}
// destroy widgets first
@@ -397,7 +419,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
@@ -427,17 +453,21 @@ ListRenderer.include({
var recordWidgets = this.allFieldWidgets[recordID];
toggleWidgets(true);
- var prom = new Promise(function (resolve, reject) {
+ return new Promise(function (resolve, reject) {
self.trigger_up('save_line', {
recordID: recordID,
onSuccess: resolve,
onFailure: reject,
});
- });
- prom.guardedCatch(function() {
+ }).then(function (changedFields) {
+ self._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.
+ return changedFields && changedFields.length && self.inMultipleRecordEdition(recordID);
+ }).guardedCatch(function () {
toggleWidgets(false);
});
- return prom;
function toggleWidgets(disabled) {
_.each(recordWidgets, function (widget) {
@@ -464,6 +494,14 @@ ListRenderer.include({
//--------------------------------------------------------------------------
/**
+ * 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.
*
@@ -477,6 +515,12 @@ ListRenderer.include({
}
},
/**
+ * @private
+ */
+ _enableRecordSelectors: function () {
+ this.$('.o_list_record_selector input').attr('disabled', false);
+ },
+ /**
* Returns the relative width according to the widget or the field type.
* @see _renderHeader
*
@@ -992,7 +1036,10 @@ ListRenderer.include({
var recordId = this._getRecordID(rowIndex);
// To select a row, the currently selected one must be unselected first
var self = this;
- return this.unselectRow().then(function () {
+ return this.unselectRow().then(function (noSelectNext) {
+ if (noSelectNext) {
+ return Promise.resolve();
+ }
if (!recordId) {
// The row to selected doesn't exist anymore (probably because
// an onchange triggered when unselecting the previous one
@@ -1003,7 +1050,10 @@ ListRenderer.include({
return new Promise(function (resolve) {
self.trigger_up('edit_line', {
recordId: recordId,
- onSuccess: resolve,
+ onSuccess: function () {
+ self._disableRecordSelectors();
+ resolve();
+ },
});
});
});
@@ -1035,7 +1085,10 @@ ListRenderer.include({
ev.stopPropagation();
var self = this;
- var groupId = $(ev.target).data('group-id');
+ // This method can be called when selecting the parent of the link.
+ // We need to ensure that the link is the actual target
+ var target = ev.target.tagName !== 'A' ? ev.target.getElementsByTagName('A')[0] : ev.target;
+ var groupId = target.dataset.groupId;
this.currentGroupId = groupId;
this.unselectRow().then(function () {
self.trigger_up('add_record', {
@@ -1213,12 +1266,13 @@ 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);
correspondingRow.children().eq(cellIndex).focus();
} else if (self.currentGroupId) {
- self.$('a[data-group-id=' + self.currentGroupId + ']').focus();
+ self.$('a[data-group-id="' + self.currentGroupId + '"]').focus();
} else {
self.$('.o_field_x2many_list_row_add a:first').focus(); // FIXME
}
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 25fdf28954c..7a2e0c51ada 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 afa8f09a857..8aaf09fc4c7 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -417,6 +417,7 @@ var ListRenderer = BasicRenderer.extend({
data: record.data,
escape: true,
isPassword: 'password' in node.attrs,
+ digits: node.attrs.digits ? JSON.parse(node.attrs.digits) : undefined,
});
this._handleAttributes($td, node);
var title = field.type !== 'boolean' ? formattedValue : '';
@@ -979,11 +980,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();
},
@@ -1156,9 +1164,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
@@ -1175,11 +1180,13 @@ var ListRenderer = BasicRenderer.extend({
_onToggleGroup: function (ev) {
ev.preventDefault();
ev.stopPropagation();
+ var self = this;
var group = $(ev.currentTarget).closest('tr').data('group');
if (group.count) {
this.trigger_up('toggle_group', {
group: group,
onSuccess: function() {
+ self._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/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index f665bf49791..abfad01558e 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -329,6 +329,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;
},
@@ -412,6 +413,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, {
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index ac8e83c8e30..bde1d793374 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -12,7 +12,6 @@ var DateWidget = Widget.extend({
template: "web.datepicker",
type_of_date: "date",
events: {
- 'change.datetimepicker': 'changeDatetime',
'error.datetimepicker': 'errorDatetime',
'change .o_datepicker_input': 'changeDatetime',
'click input': '_onInputClicked',
@@ -92,6 +91,13 @@ var DateWidget = Widget.extend({
* set datetime value
*/
changeDatetime: function () {
+ if (this.__libInput > 0) {
+ if (this.options.warn_future) {
+ this._warnFuture(this.getValue());
+ }
+ this.trigger("datetime_changed");
+ return;
+ }
var oldValue = this.getValue();
if (this.isValid()) {
this._setValueFromUi();
@@ -254,6 +260,7 @@ var DateWidget = Widget.extend({
*/
_onDateTimePickerHide: function () {
this.__isOpen = false;
+ this.changeDatetime();
if (this._onScroll) {
window.removeEventListener('scroll', this._onScroll, true);
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 5eff2fb4422..657ad2d6ebb 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -712,8 +712,8 @@
<t t-name="FieldMany2One">
<t t-if="widget.mode === 'readonly'">
- <a t-if="!widget.nodeOptions.no_open" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/>
- <span t-if="widget.nodeOptions.no_open"/>
+ <a t-if="!widget.noOpen" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/>
+ <span t-if="widget.noOpen"/>
</t>
<div t-if="widget.mode === 'edit'" class="o_field_widget o_field_many2one" aria-atomic="true">
<div class="o_input_dropdown">