summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/basic')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js214
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js46
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js44
3 files changed, 271 insertions, 33 deletions
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 97e0ca0f4be..33f6727987e 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -16,7 +16,7 @@ var Pager = require('web.Pager');
var _t = core._t;
-return AbstractController.extend(FieldManagerMixin, {
+var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
reload: '_onReload',
}),
@@ -34,7 +34,7 @@ return AbstractController.extend(FieldManagerMixin, {
this.hasButtons = params.hasButtons;
FieldManagerMixin.init.call(this, this.model);
this.handle = params.initialState.id;
- this.isDirty = false;
+ this.mode = params.mode || 'readonly';
this.mutex = new concurrency.Mutex();
},
/**
@@ -51,6 +51,82 @@ return AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
+ * Determines if we can discard the current changes. If the model is not
+ * dirty, that is not a problem. However, if it is dirty, we have to ask
+ * the user for confirmation.
+ *
+ * @override
+ * @param {string} [recordID] - default to main recordID
+ * @returns {Deferred<boolean>}
+ * resolved if can be discarded, a boolean value is given to tells
+ * if there is something to discard or not
+ * rejected otherwise
+ */
+ canBeDiscarded: function (recordID) {
+ if (!this.model.isDirty(recordID || this.handle)) {
+ return $.when(false);
+ }
+
+ var message = _t("The record has been modified, your changes will be discarded. Are you sure you want to ?");
+ var def = $.Deferred();
+ var dialog = Dialog.confirm(this, message, {
+ title: _t("Warning"),
+ confirm_callback: def.resolve.bind(def, true),
+ cancel_callback: def.reject.bind(def),
+ });
+ dialog.on('closed', def, def.reject);
+ return def;
+ },
+ /**
+ * Ask the renderer if all associated field widget are in a valid state for
+ * saving (valid value and non-empty value for required fields). If this is
+ * not the case, this notifies the user with a warning containing the names
+ * of the invalid fields.
+ *
+ * Note: changing the style of invalid fields is the renderer's job.
+ *
+ * @param {string} [recordID] - default to main recordID
+ * @return {boolean}
+ */
+ canBeSaved: function (recordID) {
+ var fieldNames = this.renderer.canBeSaved(recordID || this.handle);
+ if (fieldNames.length) {
+ this._notifyInvalidFields(fieldNames);
+ return false;
+ }
+ return true;
+ },
+ /**
+ * Discards the changes made to the record whose ID is given, if necessary.
+ * Automatically leaves to default mode for the given record.
+ *
+ * @param {string} [recordID] - default to main recordID
+ * @param {Object} [options]
+ * @param {boolean} [options.readonlyIfRealDiscard=false]
+ * After discarding record changes, the usual option is to make the
+ * record readonly. However, the view manager calls this function
+ * at inappropriate times in the current code and in that case, we
+ * don't want to go back to readonly if there is nothing to discard
+ * (e.g. when switching record in edit mode in form view, we expect
+ * the new record to be in edit mode too, but the view manager calls
+ * this function as the URL changes...) @todo get rid of this when
+ * the view manager is improved.
+ * @returns {Deferred}
+ */
+ discardChanges: function (recordID, options) {
+ var self = this;
+ recordID = recordID || this.handle;
+ return this.canBeDiscarded(recordID).then(function (needDiscard) {
+ if (needDiscard) { // Just some optimization
+ self.model.discardChanges(recordID);
+ }
+ if (options && options.readonlyIfRealDiscard && !needDiscard) {
+ return;
+ }
+ self._setMode('readonly', recordID);
+ });
+ },
+ /**
* @override
*/
renderPager: function ($node, options) {
@@ -74,11 +150,32 @@ return AbstractController.extend(FieldManagerMixin, {
this._updatePager(); // to force proper visibility
},
/**
+ * Saves the record whose ID is given if necessary (@see _saveRecord).
+ *
+ * @param {string} [recordID] - default to main recordID
+ * @param {Object} [options]
+ * @returns {Deferred}
+ */
+ saveRecord: function (recordID, options) {
+ // Some field widgets (e.g. 'html') can't detect (all) their changes, so
+ // we ask them to commit their current value before saving. This has to
+ // be done outside of the mutex protection of saving because
+ // commitChanges will trigger changes and these are also protected. So
+ // the actual saving has to be done after these changes. Also the
+ // commitChanges operation might not be synchronous for other reason
+ // (e.g. the x2m fields will ask the user if some discarding has to be
+ // made). This operation must also be mutex-protected as commitChanges
+ // function of x2m has to be aware of all final changes made to a row.
+ this.mutex.exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle)); // TODO write a test for this
+ return this.mutex.exec(this._saveRecord.bind(this, recordID, options));
+ },
+ /**
* @override
* @returns {Deferred}
*/
update: function (params, options) {
var self = this;
+ this.mode = params.mode || this.mode;
return this._super(params, options).then(function () {
self._updateEnv();
self._updatePager();
@@ -90,6 +187,24 @@ return AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
+ * Does the necessary action when trying to "abandon" a given record (e.g.
+ * when trying to make a new record readonly without having saved it). By
+ * default, if the abandoned record is the main view one, the only possible
+ * action is to leave the current view. Otherwise, it is a x2m line, ask the
+ * model to remove it.
+ *
+ * @private
+ * @param {string} [recordID] - default to main recordID
+ */
+ _abandonRecord: function (recordID) {
+ recordID = recordID || this.handle;
+ if (recordID === this.handle) {
+ this.trigger_up('switch_to_previous_view');
+ } else {
+ this.model.removeLine(recordID);
+ }
+ },
+ /**
* We override applyChanges (from the field manager mixin) to protect it
* with a mutex.
*
@@ -184,6 +299,24 @@ return AbstractController.extend(FieldManagerMixin, {
return state.count !== 0;
},
/**
+ * Helper function to display a warning that some fields have an invalid
+ * value. This is used when a save operation cannot be completed.
+ *
+ * @private
+ * @param {string[]} invalidFields - list of field names
+ */
+ _notifyInvalidFields: function (invalidFields) {
+ var record = this.model.get(this.handle, {raw: true});
+ var fields = record.fields;
+ var warnings = invalidFields.map(function (fieldName) {
+ var fieldStr = fields[fieldName].string;
+ return _.str.sprintf('<li>%s</li>', _.escape(fieldStr));
+ });
+ warnings.unshift('<ul>');
+ warnings.push('</ul>');
+ this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
+ },
+ /**
* Hook method, called when record(s) has been deleted.
*
* @see _deleteRecord
@@ -193,6 +326,67 @@ return AbstractController.extend(FieldManagerMixin, {
this.update({});
},
/**
+ * Saves the record whose ID is given, if necessary. Automatically leaves
+ * edit mode for the given record, unless told otherwise.
+ *
+ * @param {string} [recordID] - default to main recordID
+ * @param {Object} [options]
+ * @param {boolean} [options.stayInEdit=false]
+ * if true, leave the record in edit mode after save
+ * @param {boolean} [options.reload=true]
+ * if true, reload the record after (real) save
+ * @param {boolean} [options.savePoint=false]
+ * if true, the record will only be 'locally' saved: its changes
+ * will move from the _changes key to the data key
+ * @returns {Deferred}
+ */
+ _saveRecord: function (recordID, options) {
+ recordID = recordID || this.handle;
+ options = _.defaults(options || {}, {
+ stayInEdit: false,
+ reload: true,
+ savePoint: false,
+ });
+
+ var def = $.Deferred();
+ // Check if the view is in a valid state for saving
+ // Note: it is the model's job to do nothing if there is nothing to save
+ if (this.canBeSaved(recordID)) {
+ def = this.model.save(recordID, { // Save then leave edit mode
+ reload: options.reload,
+ savePoint: options.savePoint,
+ });
+ } else {
+ def.reject(); // Cannot be saved, do nothing at all
+ }
+
+ if (options.stayInEdit) {
+ return def;
+ } else {
+ return def.then(this._setMode.bind(this, 'readonly', recordID));
+ }
+ },
+ /**
+ * Change the mode for the record associated to the given ID.
+ * If the given recordID is the view's main one, then the whole view mode is
+ * changed (@see BasicController.update).
+ *
+ * @private
+ * @param {string} mode - 'readonly' or 'edit'
+ * @param {string} [recordID]
+ */
+ _setMode: function (mode, recordID) {
+ recordID = recordID || this.handle;
+ // If trying to make a temporary record readonly, discard the record
+ if (mode === 'readonly' && this.model.isNew(recordID)) {
+ this._abandonRecord(recordID);
+ return;
+ }
+ if (recordID === this.handle) {
+ this.update({mode: mode}, {reload: false});
+ }
+ },
+ /**
* Helper method, to get the current environment variables from the model
* and notifies the component chain (by bubbling an event up)
*/
@@ -226,15 +420,17 @@ return AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
- * We need to keep manually track of the isDirty property. Note that it is
- * not the same as the one from the basic model. A new record from the
- * basic model is considered dirty, but from the controller point of view,
- * there was no user interaction and should not be dirty.
+ * Forces to save directly the changes if the controller is in readonly,
+ * because in that case the changes come from widgets that are editable even
+ * in readonly (e.g. Priority).
*
* @private
+ * @param {OdooEvent}
*/
- _onFieldChanged: function () {
- this.isDirty = true;
+ _onFieldChanged: function (ev) {
+ if (this.mode === 'readonly') {
+ ev.data.force_save = true;
+ }
FieldManagerMixin._onFieldChanged.apply(this, arguments);
},
/**
@@ -261,7 +457,7 @@ return AbstractController.extend(FieldManagerMixin, {
this.reload({fieldNames: data.fieldNames});
}
},
-
});
+return BasicController;
});
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 ea8986f9da8..f3e69a6b453 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -219,7 +219,8 @@ var BasicModel = AbstractModel.extend({
discardChanges: function (id, options) {
options = options || {};
var element = this.localData[id];
- var rollback = options.rollback || this.isNew(id);
+ var isNew = this.isNew(id);
+ var rollback = options.rollback || isNew;
this._visitChildren(element, function (elem) {
if (rollback && elem._savePoint) {
if (elem._savePoint instanceof Array) {
@@ -227,8 +228,10 @@ var BasicModel = AbstractModel.extend({
} else {
elem._changes = _.extend({}, elem._savePoint);
}
+ elem._isDirty = !isNew;
} else {
elem._changes = null;
+ elem._isDirty = false;
}
});
},
@@ -383,26 +386,26 @@ var BasicModel = AbstractModel.extend({
return element;
},
/**
- * return true if a record is dirty. A record is considered dirty if it has
- * some unsaved changes. A list is considered dirty if its _changes key is
- * set to an array of its new datapoints (possibly empty)
+ * Returns true if a record is dirty. A record is considered dirty if it has
+ * some unsaved changes, marked by the _isDirty property on the record or
+ * one of its subrecords.
*
- * @param {string} id id for a local resource
+ * @param {string} id - the local resource id
* @returns {boolean}
*/
isDirty: function (id) {
var isDirty = false;
- var record = this.localData[id];
- this._visitChildren(record, function (r) {
- if (r.type === "record" ? !_.isEmpty(r._changes) : r._changes) {
+ this._visitChildren(this.localData[id], function (r) {
+ if (r._isDirty) {
isDirty = true;
}
});
return isDirty;
},
/**
- * Check if a record is new, meaning if it is in the process of being created
- * and no actual record exists in db.
+ * Check if a localData is new, meaning if it is in the process of being
+ * created and no actual record exists in db. Note: if the localData is not
+ * of the "record" type, then it is always considered as not new.
*
* Note: A virtual id is a character string composed of an integer and has
* a dash and other information.
@@ -413,7 +416,11 @@ var BasicModel = AbstractModel.extend({
* @returns {boolean}
*/
isNew: function (id) {
- var res_id = this.localData[id].res_id;
+ var data = this.localData[id];
+ if (data.type !== "record") {
+ return false;
+ }
+ var res_id = data.res_id;
if (typeof res_id === 'number') {
return false;
} else if (typeof res_id === 'string' && /^[0-9]+-/.test(res_id)) {
@@ -707,15 +714,20 @@ var BasicModel = AbstractModel.extend({
record.res_ids.push(id);
record.count++;
}
+
+ // Update the data directly or reload them
+ var def;
if (shouldReload) {
- // erase changes as they have been applied
- record._changes = {};
- return self._fetchRecord(record);
+ def = self._fetchRecord(record);
} else {
_.extend(record.data, record._changes);
- record._changes = {};
- return false;
}
+
+ // Erase changes as they have been applied
+ record._changes = {};
+ record._isDirty = false;
+
+ return def;
});
} else {
return $.when(record_id);
@@ -837,6 +849,7 @@ var BasicModel = AbstractModel.extend({
var field;
var defs = [];
record._changes = record._changes || {};
+ record._isDirty = true;
// apply changes to local data
for (var fieldName in changes) {
@@ -874,6 +887,7 @@ var BasicModel = AbstractModel.extend({
_.each(fieldNames, function (name) {
if (record._changes && record._changes[name] === record.data[name]) {
delete record._changes[name];
+ record._isDirty = !_.isEmpty(record._changes);
}
});
return self._fetchSpecialData(record).then(function (fieldNames2) {
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 dd0f59a4176..ed9726478a0 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -36,6 +36,41 @@ var BasicRenderer = AbstractRenderer.extend({
this.mode = params.mode || 'readonly';
},
/**
+ * This method has two responsabilities: find every invalid fields in the
+ * current view, and making sure that they are displayed as invalid, by
+ * toggling the o_form_invalid css class. It has to be done both on the
+ * widget, and on the label, if any.
+ *
+ * @param {string} recordID
+ * @returns {string[]} the list of invalid field names
+ */
+ canBeSaved: function (recordID) {
+ var self = this;
+ var invalidFields = [];
+ _.each(this.allFieldWidgets[recordID], function (widget) {
+ var canBeSaved = self._canWidgetBeSaved(widget);
+ if (!canBeSaved) {
+ invalidFields.push(widget.name);
+ }
+ widget.$el.toggleClass('o_form_invalid', !canBeSaved);
+ });
+ return invalidFields;
+ },
+ /**
+ * Calls 'commitChanges' on all field widgets, so that they can notify the
+ * environment with their current value (useful for widgets that can't
+ * detect when their value changes, e.g. field 'html').
+ *
+ * @param {string} recordID
+ * @return {Deferred}
+ */
+ commitChanges: function (recordID) {
+ var defs = _.map(this.allFieldWidgets[recordID], function (widget) {
+ return widget.commitChanges();
+ });
+ return $.when.apply($, defs);
+ },
+ /**
* Updates the internal state of the renderer to the new state. By default,
* this also implements the recomputation of the modifiers and their
* application to the DOM and the reset of the field widgets if needed.
@@ -222,14 +257,7 @@ var BasicRenderer = AbstractRenderer.extend({
*/
_canWidgetBeSaved: function (widget) {
var modifiers = this._getEvaluatedModifiers(widget.__node, widget.record);
- var isSetOrNotRequired = (widget.isSet() || !modifiers.required);
- var isValid = widget.isValid();
- if (isValid instanceof $.Deferred) {
- return isValid.then(function (isValid) {
- return isValid && isSetOrNotRequired;
- });
- }
- return isValid && isSetOrNotRequired;
+ return widget.isValid() && (widget.isSet() || !modifiers.required);
},
/**
* Updates the modifiers evaluation associated to a given modifiers data and