summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/fields/relational_fields.js
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2017-04-20 13:53:41 +0200
committerqsm-odoo <qsm@odoo.com>2017-04-28 16:53:43 +0200
commit03c4c855b96c0d5d1e79d9cc3ac215abbd55cfb1 (patch)
tree57c3135a3e26b6886635eb52115c70f634f9098a /addons/web/static/src/js/fields/relational_fields.js
parent0ed992d55926a176f58e02872f6fe662ce6398e1 (diff)
[REF] web: restore editable list views behaviors
Since the new views, the editable list view lost some behaviors. These behaviors were indeed implemented in the form controller/renderer but as the new editable list view does not use an inline form view, these behaviors had to be implemented in the basic controller/renderer. For this to work, the list editable renderer had to be changed as it was doing work that should be done by the controller. The problem is even more complex because the x2m fields are using a list renderer but not a list controller. So moving code from the renderer to the controller obviously broke the x2m fields. Right now, the problem is solved by catching renderer events and forwarding new ones to the form controller (which handles the x2m specifically). The initial goal of this commit was to share the validation of records on save. Indeed, the fields were marked as invalid in the form view but not in the list view. Also, before the new views, the editable list view had different behaviors if they were used for a x2m field or not. As these behaviors are making sense, this commit tries to restore them. Basically, what we want is: - When a record is saved (form view save or list view line leaving), the invalid fields are marked (in red), the names of the related fields are notified to the user and the record is not left. - When a record is discarded (form view discard or list view discard), the user is asked to confirm if the record is dirty before making the record readonly. - When a record is discarded, if the record is a new one, then the record should be abandoned (removed as if never existed). In the form view this induces to go back in the history and in the list view, to remove a row. - For x2m fields, the notification of invalid fields is not triggered but the user is instead asked if he wants to discard the changes made to the row (indeed, this replaces the list "Discard" button, as non existent for x2m lists). - ... Saving, discarding, marking the fields as invalid and other behaviors are thus now shared behavior of basic views. The management of the dirty flag has also been moved to the model as it was handle by the controller for the form view but by the renderer for the list view. Now this flag is directly managed in the basic model (the model can have changes thanks to the `_changes` property but not be dirty (this is the case for creations)). This change however created a problem. The view manager is currently keeping asking if there are changes to discard at each action which might lead to leave a dirty record (appswitcher / url change / ...). It however did not discard anything as leaving if the user is ok with it will lead to an implicit discard. However, as the view manager might ask for this discard multiple times by second, the controller was marking the record as not dirty the first time but without discarding the changes. This is more complex to do now, as the dirty state is part of the model and that the renderer should match the model data. To solve this problem, the view manager now actually discard changes explicitely when asked to. Even though this had been optimized to not cause any rerender in some cases where it is not needed, this could cause some performance decreasing. However, this makes some cases more logical (opening the app switcher on a dirty form view then going back to the form view by hitting the "go back" button left the form view untouched although the user asked to discard it). This solution will be improved with the view manager refactoring. This commit is also making use of the `commitChanges` system which had been implemented for HTML fields. Indeed, these fields cannot know about all of their changes, so when hitting the save button, we asked those fields to commit their value. Using this system is a great way to make the `isValid` method of x2m fields synchronous. Indeed, before this commit, the method was sometimes asking the user if he wants to discard an invalid line before save. That case can be handled by the x2m `commitChanges` method: we consider that saving the lines of x2m is an operation that has to be done before considering the save, so we ask all the x2m fields to do so at that time. Also, the system was broken since a recent commit: we indeed protected the changes - save order with a mutex but unfortunately, the `commitChanges` method was part of the save and the changes it triggered were not able to be considered because of this mutex. This had not been detected by tests as there is not current way to test html fields.
Diffstat (limited to 'addons/web/static/src/js/fields/relational_fields.js')
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js166
1 files changed, 113 insertions, 53 deletions
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 6a7ea1869b7..2a3d5e01125 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -517,11 +517,13 @@ var KanbanFieldMany2One = AbstractField.extend({
var FieldX2Many = AbstractField.extend({
tagName: 'div',
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
+ add_record: '_onAddRecord',
+ edit_line: '_onEditLine',
field_changed: '_onFieldChanged',
kanban_record_delete: '_onDeleteRecord',
list_record_delete: '_onDeleteRecord',
- add_record: '_onAddRecord',
open_record: '_onOpenRecord',
+ save_line: '_onSaveLine',
toggle_column_order: '_onToggleColumnOrder',
}),
@@ -563,31 +565,29 @@ var FieldX2Many = AbstractField.extend({
//--------------------------------------------------------------------------
/**
+ * A x2m field can only be saved if it finished the edition of all its rows.
+ * On parent view saving, we have to ask the x2m fields to commit their
+ * changes, that is confirming the save of the in-edition row or asking the
+ * user if he wants to discard it if necessary.
+ *
* @override
+ * @returns {Deferred}
*/
- isSet: function () {
- return true;
+ commitChanges: function () {
+ var inEditionRecordID =
+ this.renderer
+ && this.renderer.viewType === "list"
+ && this.renderer.getEditableRecordID();
+ if (inEditionRecordID) {
+ return this._saveLine(inEditionRecordID);
+ }
+ return this._super.apply(this, arguments);
},
/**
- * This is a tricky override. For relational fields, we don't just need to
- * know if the field is valid. We also need to discard some invalid lines,
- * if the user is ok with it (or automatically if the invalid line is not
- * dirty). This is the reason why we need to change the signature of the
- * AbstractField isValid method to optionally return a deferred.
- *
* @override
- * @returns {boolean|Deferred} if there is a renderer (sub list or kanban
- * view), we may need to ask the user => it will return a deferred.
- * Otherwise, it returns a boolean
*/
- isValid: function () {
- if (!this.renderer) {
- return this._super.apply(this, arguments);
- }
- var def = $.Deferred();
- this.renderer.canBeSaved()
- .then(def.resolve.bind(def, true), def.resolve.bind(def, false));
- return def;
+ isSet: function () {
+ return true;
},
//--------------------------------------------------------------------------
@@ -687,6 +687,39 @@ var FieldX2Many = AbstractField.extend({
this.$buttons.on('click', 'button.o-kanban-button-new', this._onAddRecord.bind(this));
}
},
+ /**
+ * Saves the line associated to the given recordID. If the line is valid,
+ * it only has to be switched to readonly mode as all the line changes have
+ * already been notified to the model so that they can be saved in db if the
+ * parent view is actually saved. If the line is not valid, the line is to
+ * be discarded if the user agrees (this behavior is not a list editable
+ * one but a x2m one as it is made to replace the "discard" button which
+ * exists for list editable views).
+ *
+ * @private
+ * @param {string} recordID
+ * @returns {Deferred} resolved if the line was properly saved or discarded.
+ * rejected if the line could not be saved and the user
+ * did not agree to discard.
+ */
+ _saveLine: function (recordID) {
+ var self = this;
+ var def = $.Deferred();
+ this.renderer.commitChanges(recordID).then(function () { // TODO wrong as no mutex protection
+ var fieldNames = self.renderer.canBeSaved(recordID);
+ if (fieldNames.length) {
+ self.trigger_up('discard_x2m_changes', {
+ recordID: recordID,
+ onSuccess: def.resolve.bind(def),
+ onFailure: def.reject.bind(def),
+ });
+ } else {
+ self.renderer.setRowMode(recordID, 'readonly', true);
+ def.resolve();
+ }
+ });
+ return def;
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -708,33 +741,44 @@ var FieldX2Many = AbstractField.extend({
* by the parent controller.
*
* @private
- * @param {OdooEvent}
+ * @param {OdooEvent} ev
*/
- _onDeleteRecord: function (event) {
- event.stopPropagation();
+ _onDeleteRecord: function (ev) {
+ ev.stopPropagation();
this._setValue({
operation: 'REMOVE',
- ids: [event.data.id],
+ ids: [ev.data.id],
});
},
/**
+ * Called when the renderer asks to edit a line, in that case simply tells
+ * him back to toggle the mode of this row.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onEditLine: function (ev) {
+ ev.stopPropagation();
+ this.renderer.setRowMode(ev.data.recordID, 'edit');
+ },
+ /**
* Updates the given record with the changes.
*
* @private
- * @param {OdooEvent}
+ * @param {OdooEvent} ev
*/
- _onFieldChanged: function (event) {
- if (event.target === this) {
+ _onFieldChanged: function (ev) {
+ if (ev.target === this) {
return;
}
- event.stopPropagation();
+ ev.stopPropagation();
// changes occured in an editable list
- var changes = event.data.changes;
+ var changes = ev.data.changes;
if (Object.keys(changes).length) {
this._setValue({
operation: 'UPDATE',
- id: event.data.dataPointID,
- data: changes
+ id: ev.data.dataPointID,
+ data: changes,
});
}
},
@@ -748,14 +792,30 @@ var FieldX2Many = AbstractField.extend({
// to implement
},
/**
+ * 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,
+ * we trigger a new event for the view to tell it to discard the changes
+ * made to that row.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSaveLine: function (ev) {
+ ev.stopPropagation();
+ this._saveLine(ev.data.recordID)
+ .done(ev.data.onSuccess)
+ .fail(ev.data.onFailure);
+ },
+ /**
* Adds field name information to the event, so that the view upstream is
* aware of which widgets it has to redraw.
*
* @private
- * @param {OdooEvent}
+ * @param {OdooEvent} ev
*/
- _onToggleColumnOrder: function (event) {
- event.data.field = this.name;
+ _onToggleColumnOrder: function (ev) {
+ ev.data.field = this.name;
},
});
@@ -769,13 +829,13 @@ var FieldOne2Many = FieldX2Many.extend({
/**
* @override
* @param {Object} record
- * @param {OdooEvent} [event] an event that triggered the reset action
+ * @param {OdooEvent} [ev] an event that triggered the reset action
* @returns {Deferred}
*/
- reset: function (record, event) {
+ reset: function (record, ev) {
var self = this;
- if (event && event.target === this && self.view.arch.tag === 'tree' && this.editable) {
- var command = event.data.changes[this.name];
+ if (ev && ev.target === this && ev.data.changes && self.view.arch.tag === 'tree' && this.editable) {
+ var command = ev.data.changes[this.name];
if (command.operation === 'UPDATE') {
var state = record.data[this.name];
var fieldNames = state.getFieldNames();
@@ -784,8 +844,8 @@ var FieldOne2Many = FieldX2Many.extend({
}
}
return this._super.apply(this, arguments).then(function () {
- if (event && event.target === self && self.view.arch.tag === 'tree') {
- if (event.data.changes[self.name].operation === 'CREATE') {
+ if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') {
+ if (ev.data.changes[self.name].operation === 'CREATE') {
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
@@ -833,13 +893,13 @@ var FieldOne2Many = FieldX2Many.extend({
*
* @override
* @private
- * @param {OdooEvent|MouseEvent} event this event comes either from the 'Add
+ * @param {OdooEvent|MouseEvent} ev this event comes either from the 'Add
* record' link in the list editable renderer, or from the 'Create' button
* in the kanban view
*/
- _onAddRecord: function (event) {
+ _onAddRecord: function (ev) {
// we don't want interference with the components upstream.
- event.stopPropagation();
+ ev.stopPropagation();
if (this.editable) {
this._setValue({
@@ -863,14 +923,14 @@ var FieldOne2Many = FieldX2Many.extend({
* form view.
*
* @private
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
*/
- _onOpenRecord: function (event) {
+ _onOpenRecord: function (ev) {
// we don't want interference with the components upstream.
- event.stopPropagation();
+ ev.stopPropagation();
this._openFormDialog({
- id: event.data.id,
+ id: ev.data.id,
on_saved: this._setValue.bind(this, { operation: 'NOOP' }),
readonly: this.mode === 'readonly',
});
@@ -932,14 +992,14 @@ var FieldMany2Many = FieldX2Many.extend({
* to the form view.
*
* @private
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
*/
- _onOpenRecord: function (event) {
- _.extend(event.data, {
+ _onOpenRecord: function (ev) {
+ _.extend(ev.data, {
context: this.record.getContext(this.recordParams),
domain: this.record.getDomain(this.recordParams),
fields_view: this.attrs.views && this.attrs.views.form,
- on_saved: this.trigger_up.bind(this, 'reload', {db_id: event.data.id}),
+ on_saved: this.trigger_up.bind(this, 'reload', {db_id: ev.data.id}),
readonly: this.mode === 'readonly',
string: this.string,
});
@@ -1297,9 +1357,9 @@ var FieldMany2ManyTags = AbstractField.extend({
* @param {KeyboardEvent} event
*/
_onKeyDown: function (event) {
- if($(event.target).val() === "" && event.which === $.ui.keyCode.BACKSPACE) {
+ if ($(event.target).val() === "" && event.which === $.ui.keyCode.BACKSPACE) {
var $badges = this.$('.badge');
- if($badges.length) {
+ if ($badges.length) {
this._removeTag($badges.last().data('id'));
}
}