summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic/basic_controller.js
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2019-03-06 21:18:36 +0100
committerGitHub <noreply@github.com>2019-03-06 21:18:36 +0100
commitfc60c49ba3cb30fc4e436c9ffc1661d707fe858c (patch)
tree110bf47078ce4862cfad05aa061ba1238ebc99e6 /addons/web/static/src/js/views/basic/basic_controller.js
parent414ac1a49a61e7f68b4c8dab8688a5b8840aefed (diff)
parent53e7050ce0c1bd873f239bce8ad81ba42c19d57a (diff)
[MERGE] web,*: update code base to jQuery 3.*
The goal of this task is to update jQuery from version 1.11 to 3.x. Task-id : 1896658 As we are skipping 2 magor versions, there are some breaking changes in the jquery library described here: https://jquery.com/upgrade-guide/3.0/ Main changes are: ------------------------- 1. Update the libraries jQuery, jQueryUI, QUnit and select2 to their latest version. The link to generate the build of jQueryUI with the used components: [jquery UI build](https://jqueryui.com/download/#!version=1.12.1&components=111110010101111110100010110000011010000000000000) 2. Use native promises ([a complete explanation about promises here](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md)) everywhere possible: In order to [be A+ compliant](https://promisesaplus.com/), the jq3 deferred uses `setTimeout(..., 0)` for every then and catch, which takes between 4 and 10 ms by call. This would be way too slow to use in Odoo. To prevent this, we have used the native Promises everywhere possible. Be it with jq deferred or native promises, all promises are now asynchronous, this example will ALWAYS result in printing first "just after blabla" then "blabla" because the `then` function will always be asynchronous (opposite to what jquery used to do) ``` function () { Promise.resolve("blabla").then(console.log); console.log("just after blabla"); } // console: // just after blabla // blabla ``` 3. On native promises, you should see the catch like a try/catch in javascript, meaning that it will catch all errors including coding errors: For this we have introduced the concept of `guardedCatch`, which will be executed on promise rejection unless the promise is rejected because there is a code error or runtime error (calls on undefined, etc). 4. Native Promises do not have `done`, `fail` and `always` methods nor does it have the `state` method - `fail` can be replaced with `guardedCatch` - `return $.when(x).done(function() ...)` can be replaced with ``` var x = Promise.resolve(); // we get the x promise in a local variable, without then x.then(function() ...); // we add a new handler to x. This operation returns a new promise we don't care about return x; // we return the x promise that will be completed without waiting for the then ``` - `$.when().done(function ...)` without the return can be replaced by `then(function ....)`, because if it is not returned, it doesn't matter what is chained or not `$.when().always(function() ...)` can be replaced with ``` var always = function() ... Promise.resolve().then(always).guardedCatch(always); ``` - It is not possible to inspect the state of a promise, except in debug tools of chrome and firefox. Any code that relies on `$.Deferred.state()` should be rewritten to use flags in `then` and `catch` handlers. Remember that it is not possible to execute the handlers in a synchroneous manner, so when a promise is created, its `then` will never be called on the same execution stack. At best it will be called on the next microtask tick (before the next setTimeout). 5. We have introduced the concept of guardedCatch, to copy the behavior of the old fail: The guarded catch will only execute the handler in parameter if the rejection reason is not an `Error` ``` var failingPromise = Promise.reject({message:"I am the rejection reason"}); failingPromise.guardedCatch(function(reason) {console.log(reason);}); // is called, then returns a rejected promise failingPromise.catch(function(reason) {console.log(reason);}); // is called ``` will write the reason "I am the rejection reason" to the console twice: once because the `guardedCatch` will execute (and return a rejected `Promise` with the same reason it was called), a second time because the `catch` will catch the rejected reason returned by the `guardedCatch` ``` var promiseWithError = Promise.resolve().then(function() { a.b = 0; // <-- the variable a does not exist, so a.b is an error }); promiseWithError.guardedCatch(function(reason) {console.log(reason);}); // will not be called promiseWithError.catch(function(reason) {console.log(reason);}); // will be called ``` contrary to how deferred used to work, here the catch will be called because it behaves like a `try... catch...` and will catch the coding error like if it was a business error. The `guardedCatch` however does not call the handler if the rejection reason is an error. In all Odoo code, we never reject a promise with an error and always use objects to defined the rejected reason, so it is a good idea to use `guardedCatch` everywhere. 6. Native Promises can have only one parameter passed to a `then` function or a `catch` function: `xxx.then(function(param1, param2) {...}); // param2 will never have a value` - we can replace `$.when(prom1, prom2).then(function(result1, result2) { ... });` by ``` Promise.all([prom1, prom2]).then(function(results) { var result1 = results[1]; var result2 = results[2]; ... }); ``` 7. We are now using ES7 code in the unit tests (async await). closes odoo/odoo#28457 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Christophe Matthieu <chm@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: David Monjoie <dmo@odoo.com> Co-authored-by: Martin Geubelle <mge@odoo.com> Co-authored-by: svs-odoo <svs@odoo.com> Co-authored-by: Vincent Schippefilt <vsc@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/views/basic/basic_controller.js')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js112
1 files changed, 59 insertions, 53 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 07646c83273..edfaba60b54 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -41,11 +41,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// savingDef is used to ensure that we always wait for pending save
// operations to complete before checking if there are changes to
// discard when discardChanges is called
- this.savingDef = $.when();
+ this.savingDef = Promise.resolve();
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
// add classname to reflect the (absence of) access rights (used to
@@ -66,24 +66,27 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
*
* @override
* @param {string} [recordID] - default to main recordID
- * @returns {Deferred<boolean>}
+ * @returns {Promise<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) {
+ var self = this;
if (!this.isDirty(recordID)) {
- return $.when(false);
+ return Promise.resolve(false);
}
var message = _t("The record has been modified, your changes will be discarded. Do you want to proceed?");
- 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),
+ var def;
+ def = new Promise(function (resolve, reject) {
+ var dialog = Dialog.confirm(self, message, {
+ title: _t("Warning"),
+ confirm_callback: resolve.bind(self, true),
+ cancel_callback: reject,
+ });
+ dialog.on('closed', def, reject);
});
- dialog.on('closed', def, def.reject);
return def;
},
/**
@@ -114,7 +117,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @see _.discardChanges
*/
discardChanges: function (recordID, options) {
- return $.when(this.mutex.getUnlockedDef(), this.savingDef)
+ return Promise.all([this.mutex.getUnlockedDef(), this.savingDef])
.then(this._discardChanges.bind(this, recordID || this.handle, options));
},
/**
@@ -144,6 +147,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @override
*/
renderPager: function ($node, options) {
+ var self = this;
var data = this.model.get(this.handle, {raw: true});
this.pager = new Pager(this, data.count, data.offset + 1, data.limit, options);
@@ -161,19 +165,21 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
})
.then(this.pager.enable.bind(this.pager));
});
- this.pager.appendTo($node);
- this._updatePager(); // to force proper visibility
+ return this.pager.appendTo($node).then(function() {
+ self._updatePager(); // to force proper visibility
+ });
},
/**
* Saves the record whose ID is given if necessary (@see _saveRecord).
*
* @param {string} [recordID] - default to main recordID
* @param {Object} [options]
- * @returns {Deferred}
+ * @returns {Promise}
* Resolved with the list of field names (whose value has been modified)
* Rejected if the record can't be saved
*/
saveRecord: function (recordID, options) {
+ var self = this;
// Some field widgets can't detect (all) their changes immediately or
// may have to validate them before notifying them, so we ask them to
// commit their current value before saving. This has to be done outside
@@ -187,23 +193,22 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// 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.
- var self = this;
- var savingDef = $.Deferred();
- this.savingDef = savingDef;
-
- return this.mutex.getUnlockedDef()
+ var unlockedMutex = this.mutex.getUnlockedDef()
.then(function () {
return self.renderer.commitChanges(recordID || self.handle);
})
.then(function () {
return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
- })
- .then(savingDef.resolve.bind(savingDef))
- .fail(savingDef.resolve.bind(savingDef));
+ });
+ this.savingDef = new Promise(function (resolve) {
+ unlockedMutex.then(resolve).guardedCatch(resolve);
+ });
+
+ return unlockedMutex;
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
update: function (params, options) {
var self = this;
@@ -267,31 +272,32 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private
* @param {Object} attrs the attrs of the button clicked
* @param {Object} [record] the current state of the view
- * @returns {Deferred}
+ * @returns {Promise}
*/
_callButtonAction: function (attrs, record) {
var self = this;
- var def = $.Deferred();
- var reload = function () {
- return self.isDestroyed() ? $.when() : self.reload();
- };
- record = record || this.model.get(this.handle);
+ var def = new Promise(function (resolve, reject) {
+ var reload = function () {
+ return self.isDestroyed() ? Promise.resolve() : self.reload();
+ };
+ record = record || self.model.get(self.handle);
- this.trigger_up('execute_action', {
- action_data: _.extend({}, attrs, {
- context: record.getContext({additionalContext: attrs.context || {}}),
- }),
- env: {
- context: record.getContext(),
- currentID: record.data.id,
- model: record.model,
- resIDs: record.res_ids,
- },
- on_success: def.resolve.bind(def),
- on_fail: function () {
- self.update({}, {reload: false}).always(def.reject.bind(def));
- },
- on_closed: reload,
+ self.trigger_up('execute_action', {
+ action_data: _.extend({}, attrs, {
+ context: record.getContext({additionalContext: attrs.context || {}}),
+ }),
+ env: {
+ context: record.getContext(),
+ currentID: record.data.id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
+ on_success: resolve,
+ on_fail: function () {
+ self.update({}, {reload: false}).then(reject).guardedCatch(reject);
+ },
+ on_closed: reload,
+ });
});
return this.alive(def);
},
@@ -305,7 +311,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {string} id - the id of one of the view's records
* @param {string[]} fields - the changed fields
* @param {OdooEvent} e - the event that triggered the change
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmChange: function (id, fields, e) {
if (e.name === 'discard_changes' && e.target.reset) {
@@ -364,7 +370,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* 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 webclient/action_manager's hashchange mechanism is improved.
- * @returns {Deferred}
+ * @returns {Promise}
*/
_discardChanges: function (recordID, options) {
var self = this;
@@ -459,7 +465,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @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}
+ * @returns {Promise}
* Resolved with the list of field names (whose value has been modified)
* Rejected if the record can't be saved
*/
@@ -490,7 +496,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
return saveDef;
} else {
- return $.Deferred().reject(); // Cannot be saved
+ return Promise.reject("SaveRecord: this.canBeSave is false"); // Cannot be saved
}
},
/**
@@ -501,7 +507,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private
* @param {string} mode - 'readonly' or 'edit'
* @param {string} [recordID]
- * @returns {Deferred}
+ * @returns {Promise}
*/
_setMode: function (mode, recordID) {
if ((recordID || this.handle) === this.handle) {
@@ -511,7 +517,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
core.bus.trigger('DOM_updated');
});
}
- return $.when();
+ return Promise.resolve();
},
/**
* Helper method, to get the current environment variables from the model
@@ -564,14 +570,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
ev.stopPropagation();
var recordID = ev.data.recordID;
this._discardChanges(recordID)
- .done(function () {
+ .then(function () {
// TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click
// made on another row if any
self._confirmChange(recordID, [ev.data.fieldName], ev)
- .always(ev.data.onSuccess);
+ .then(ev.data.onSuccess).guardedCatch(ev.data.onSuccess);
})
- .fail(ev.data.onFailure);
+ .guardedCatch(ev.data.onFailure);
},
/**
* Forces to save directly the changes if the controller is in readonly,