diff options
Diffstat (limited to 'addons/web/static/src/js/core')
| -rw-r--r-- | addons/web/static/src/js/core/ajax.js | 325 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/concurrency.js | 239 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/dialog.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/dom.js | 21 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/misc.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/mixins.js | 31 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/mvc.js | 11 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/rpc.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/service_mixins.js | 92 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/session.js | 68 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/translation.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/core/widget.js | 34 |
12 files changed, 427 insertions, 414 deletions
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index 509126498bc..8e7216c7b00 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -3,22 +3,19 @@ odoo.define('web.ajax', function (require) { var config = require('web.config'); var core = require('web.core'); -var utils = require('web.utils'); var time = require('web.time'); var download = require('web.download'); var contentdisposition = require('web.contentdisposition'); var _t = core._t; -function genericJsonRpc (fct_name, params, settings, fct) { +function _genericJsonRpc (fct_name, params, settings, fct) { var shadow = settings.shadow || false; delete settings.shadow; if (!shadow) { core.bus.trigger('rpc_request'); } - var deferred = $.Deferred(); - var data = { jsonrpc: "2.0", method: fct_name, @@ -26,7 +23,7 @@ function genericJsonRpc (fct_name, params, settings, fct) { id: Math.floor(Math.random() * 1000 * 1000 * 1000) }; var xhr = fct(data); - var result = xhr.pipe(function(result) { + var result = xhr.then(function(result) { core.bus.trigger('rpc:result', data, result); if (result.error !== undefined) { if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { @@ -36,65 +33,86 @@ function genericJsonRpc (fct_name, params, settings, fct) { console.error("Server application error", JSON.stringify(result.error)); } } - return $.Deferred().reject("server", result.error); + return Promise.reject({type: "server", error: result.error}); } else { return result.result; } }, function() { //console.error("JsonRPC communication error", _.toArray(arguments)); - var def = $.Deferred(); - return def.reject.apply(def, ["communication"].concat(_.toArray(arguments))); + var reason = { + type: 'communication', + error: arguments[0], + textStatus: arguments[1], + errorThrown: arguments[2], + }; + return Promise.reject(reason); }); - deferred.abort = function () { - deferred.reject({message: "XmlHttpRequestError abort"}, $.Event('abort')); - if (xhr.abort) { - xhr.abort(); - } - }; - result.then(function (result) { - if (!shadow) { - core.bus.trigger('rpc_response'); - } - deferred.resolve(result); - }, function (type, error, textStatus, errorThrown) { - if (type === "server") { + var rejection; + var promise = new Promise(function (resolve, reject) { + rejection = reject; + + result.then(function (result) { if (!shadow) { core.bus.trigger('rpc_response'); } - if (error.code === 100) { - core.bus.trigger('invalidate_session'); - } - deferred.reject(error, $.Event()); - } else { - if (!shadow) { - core.bus.trigger('rpc_response_failed'); + resolve(result); + }, function (reason) { + var type = reason.type; + var error = reason.error; + var textStatus = reason.textStatus; + var errorThrown = reason.errorThrown; + if (type === "server") { + if (!shadow) { + core.bus.trigger('rpc_response'); + } + if (error.code === 100) { + core.bus.trigger('invalidate_session'); + } + reject({message: error, event: $.Event()}); + } else { + if (!shadow) { + core.bus.trigger('rpc_response_failed'); + } + var nerror = { + code: -32098, + message: "XmlHttpRequestError " + errorThrown, + data: { + type: "xhr"+textStatus, + debug: error.responseText, + objects: [error, errorThrown] + }, + }; + reject({message: nerror, event: $.Event()}); } - var nerror = { - code: -32098, - message: "XmlHttpRequestError " + errorThrown, - data: { - type: "xhr"+textStatus, - debug: error.responseText, - objects: [error, errorThrown] - }, - }; - deferred.reject(nerror, $.Event()); - } + }); }); - deferred.fail(function () { // Allow deferred user to disable rpc_error call in fail - deferred.fail(function (error, event) { - if (!event.isDefaultPrevented()) { - core.bus.trigger('rpc_error', error, event); - } + + // FIXME: jsonp? + promise.abort = function () { + rejection({ + message: "XmlHttpRequestError abort", + event: $.Event('abort') }); + if (xhr.abort) { + xhr.abort(); + } + }; + promise.guardedCatch(function (reason) { // Allow promise user to disable rpc_error call in case of failure + setTimeout(function () { + // we want to execute this handler after all others (hence + // setTimeout) to let the other handlers prevent the event + if (!reason.event.isDefaultPrevented()) { + core.bus.trigger('rpc_error', reason.message, reason.event); + } + }, 0); }); - return deferred; -} + return promise; +}; function jsonRpc(url, fct_name, params, settings) { settings = settings || {}; - return genericJsonRpc(fct_name, params, settings, function(data) { + return _genericJsonRpc(fct_name, params, settings, function(data) { return $.ajax(url, _.extend({}, settings, { url: url, dataType: 'json', @@ -110,19 +128,6 @@ function rpc(url, params, settings) { return jsonRpc(url, 'call', params, settings); } -// helper -function realSetTimeout (fct, millis) { - var finished = new Date().getTime() + millis; - var wait = function() { - var current = new Date().getTime(); - if (current < finished) { - setTimeout(wait, finished - current); - } else { - fct(); - } - }; - setTimeout(wait, millis); -} /** * Load css asynchronously: fetch it from the url parameter and add a link tag @@ -131,26 +136,27 @@ function realSetTimeout (fct, millis) { * immediately. * * @param {String} url of the css to be fetched - * @returns {Deferred} resolved when the css has been loaded. + * @returns {Promise} resolved when the css has been loaded. */ var loadCSS = (function () { - var urlDefs = Object.create(null); + var urlDefs = {}; return function loadCSS(url) { if (url in urlDefs) { // nothing to do here } else if ($('link[href="' + url + '"]').length) { - // the link is already in the DOM, the deferred can be resolved - urlDefs[url] = $.when(); + // the link is already in the DOM, the promise can be resolved + urlDefs[url] = Promise.resolve(); } else { var $link = $('<link>', { 'href': url, 'rel': 'stylesheet', 'type': 'text/css' }); - urlDefs[url] = $.Deferred(); - $link.on('load', function () { - urlDefs[url].resolve(); + urlDefs[url] = new Promise(function (resolve, reject) { + $link.on('load', function () { + resolve(); + }); }); $('head').append($link); } @@ -159,44 +165,45 @@ var loadCSS = (function () { })(); var loadJS = (function () { - var urls = []; - var defs = []; + var dependenciesPromise = {}; var load = function loadJS(url) { // Check the DOM to see if a script with the specified url is already there var alreadyRequired = ($('script[src="' + url + '"]').length > 0); - // If loadJS was already called with the same URL, it will have a registered deferred indicating if - // the script has been fully loaded. If not, the deferred has to be initialized. This is initialized - // as already resolved if the script was already there without the need of loadJS. - var index = _.indexOf(urls, url); - if (index < 0) { - urls.push(url); - index = defs.push(alreadyRequired ? $.when() : $.Deferred()) - 1; + // If loadJS was already called with the same URL, it will have a registered promise indicating if + // the script has been fully loaded. If not, the promise has to be initialized. + // This is initialized as already resolved if the script was already there without the need of loadJS. + if (url in dependenciesPromise) { + return dependenciesPromise[url]; } + var scriptLoadedPromise = new Promise(function (resolve, reject) { + if (alreadyRequired) { + resolve(); + } else { + // Get the script associated promise and returns it after initializing the script if needed. The + // promise is marked to be resolved on script load and rejected on script error. + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.onload = script.onreadystatechange = function() { + if ((script.readyState && script.readyState !== "loaded" && script.readyState !== "complete") || script.onload_done) { + return; + } + script.onload_done = true; + resolve(url); + }; + script.onerror = function () { + console.error("Error loading file", script.src); + reject(url); + }; + var head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(script); + } + }); - // Get the script associated deferred and returns it after initializing the script if needed. The - // deferred is marked to be resolved on script load and rejected on script error. - var def = defs[index]; - if (!alreadyRequired) { - var script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = url; - script.onload = script.onreadystatechange = function() { - if ((script.readyState && script.readyState !== "loaded" && script.readyState !== "complete") || script.onload_done) { - return; - } - script.onload_done = true; - def.resolve(url); - }; - script.onerror = function () { - console.error("Error loading file", script.src); - def.reject(url); - }; - var head = document.head || document.getElementsByTagName('head')[0]; - head.appendChild(script); - } - return def; + dependenciesPromise[url] = scriptLoadedPromise; + return scriptLoadedPromise; }; return load; @@ -308,22 +315,6 @@ function get_file(options) { } function post (controller_url, data) { - - var progressHandler = function (deferred) { - return function (state) { - if(state.lengthComputable) { - deferred.notify({ - h_loaded: utils.human_size(state.loaded), - h_total : utils.human_size(state.total), - loaded : state.loaded, - total : state.total, - pcent : Math.round((state.loaded/state.total)*100) - }); - } - }; - }; - - var Def = $.Deferred(); var postData = new FormData(); $.each(data, function(i,val) { @@ -333,25 +324,20 @@ function post (controller_url, data) { postData.append('csrf_token', core.csrf_token); } - var xhr = new XMLHttpRequest(); - if(xhr.upload) xhr.upload.addEventListener('progress', progressHandler(Def), false); - - var ajaxDef = $.ajax(controller_url, { - xhr: function() {return xhr;}, - data: postData, - processData: false, - contentType: false, - type: 'POST' - }).then(function (data) {Def.resolve(data);}) - .fail(function (data) {Def.reject(data);}); - - return Def; + return new Promise(function (resolve, reject) { + $.ajax(controller_url, { + data: postData, + processData: false, + contentType: false, + type: 'POST' + }).then(resolve).fail(reject); + }); } /** * Loads an XML file according to the given URL and adds its associated qweb * templates to the given qweb engine. The function can also be used to get - * the deferred which indicates when all the calls to the function are finished. + * the promise which indicates when all the calls to the function are finished. * * Note: "all the calls" = the calls that happened before the current no-args * one + the calls that will happen after but when the previous ones are not @@ -359,8 +345,8 @@ function post (controller_url, data) { * * @param {string} [url] - an URL where to find qweb templates * @param {QWeb} [qweb] - the engine to which the templates need to be added - * @returns {Deferred} - * If no argument is given to the function, the deferred's state + * @returns {Promise} + * If no argument is given to the function, the promise's state * indicates if "all the calls" are finished (see main description). * Otherwise, it indicates when the templates associated to the given * url have been loaded. @@ -369,63 +355,64 @@ var loadXML = (function () { // Some "static" variables associated to the loadXML function var isLoading = false; var loadingsData = []; - var allLoadingsDef = $.when(); var seenURLs = []; return function (url, qweb) { - // If no argument, simply returns the deferred which indicates when + function _load() { + isLoading = true; + if (loadingsData.length) { + // There is something to load, load it, resolve the associated + // promise then start loading the next one + var loadingData = loadingsData[0]; + loadingData.qweb.add_template(loadingData.url, function () { + // Remove from array only now so that multiple calls to + // loadXML with the same URL returns the right promise + loadingsData.shift(); + loadingData.resolve(); + _load(); + }); + } else { + // There is nothing to load anymore, so resolve the + // "all the calls" promise + isLoading = false; + } + } + + // If no argument, simply returns the promise which indicates when // "all the calls" are finished if (!url || !qweb) { - return allLoadingsDef; + return Promise.resolve(); } // If the given URL has already been seen, do nothing but returning the - // associated deferred + // associated promise if (_.contains(seenURLs, url)) { var oldLoadingData = _.findWhere(loadingsData, {url: url}); - return oldLoadingData ? oldLoadingData.def : $.when(); + return oldLoadingData ? oldLoadingData.def : Promise.resolve(); } seenURLs.push(url); + // Add the information about the new data to load: the url, the qweb - // engine and the associated deferred + // engine and the associated promise var newLoadingData = { url: url, qweb: qweb, - def: $.Deferred(), }; + newLoadingData.def = new Promise(function (resolve, reject) { + newLoadingData.resolve = resolve; + newLoadingData.reject = reject; + }); loadingsData.push(newLoadingData); // If not already started, start the loading loop (reinitialize the - // "all the calls" deferred to an unresolved state) + // "all the calls" promise to an unresolved state) if (!isLoading) { - allLoadingsDef = $.Deferred(); _load(); } - // Return the deferred associated to the new given URL + // Return the promise associated to the new given URL return newLoadingData.def; - - function _load() { - isLoading = true; - if (loadingsData.length) { - // There is something to load, load it, resolve the associated - // deferred then start loading the next one - var loadingData = loadingsData[0]; - loadingData.qweb.add_template(loadingData.url, function () { - // Remove from array only now so that multiple calls to - // loadXML with the same URL returns the right deferred - loadingsData.shift(); - loadingData.def.resolve(); - _load(); - }); - } else { - // There is nothing to load anymore, so resolve the - // "all the calls" deferred - isLoading = false; - allLoadingsDef.resolve(); - } - } }; })(); @@ -444,7 +431,7 @@ var loadAsset = (function () { var load = function loadAsset(xmlId) { if (cache[xmlId]) { - return $.when(cache[xmlId]); + return Promise.resolve(cache[xmlId]); } var params = { args: [xmlId, { @@ -493,22 +480,20 @@ var loadAsset = (function () { * @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template * contains the script and link to be loaded * - * @returns {Deferred} + * @returns {Promise} */ function loadLibs (libs) { var defs = []; - _.each(libs.jsLibs || [], function (urls) { - defs.push($.when.apply($, defs).then(function () { + (libs.jsLibs || []).forEach(function (urls) { + defs.push(Promise.all(defs).then(function () { if (typeof(urls) === 'string') { return ajax.loadJS(urls); } else { - return $.when.apply($, _.map(urls, function (url) { - return ajax.loadJS(url); - })); + return Promise.all(urls.map(ajax.loadJS)); } })); }); - _.each(libs.cssLibs || [], function (url) { + (libs.cssLibs || []).forEach(function (url) { defs.push(ajax.loadCSS(url)); }); _.each(libs.assetLibs || [], function (xmlId) { @@ -516,7 +501,7 @@ function loadLibs (libs) { return loadLibs(asset); })); }); - return $.when.apply($, defs); + return Promise.all(defs); } var ajax = { diff --git a/addons/web/static/src/js/core/concurrency.js b/addons/web/static/src/js/core/concurrency.js index 6eeebe7ff58..716abafa919 100644 --- a/addons/web/static/src/js/core/concurrency.js +++ b/addons/web/static/src/js/core/concurrency.js @@ -8,7 +8,7 @@ odoo.define('web.concurrency', function (require) { * everything concurrency related in Odoo. * * The basic concurrency primitives in Odoo JS are the callback, and the - * promises. Promises (deferred) are more composable, so we usually use them + * promises. Promises (promise) are more composable, so we usually use them * whenever possible. We use the jQuery implementation. * * Those functions are really nothing special, but are simply the result of how @@ -19,54 +19,15 @@ var Class = require('web.Class'); return { /** - * The jquery implementation for $.when has a (most of the time) useful - * property: it is synchronous, if the deferred is resolved immediately. - * - * This means that when we execute $.when(def), then all registered - * callbacks will be executed before the next line is executed. This is - * useful quite often, but in some rare cases, we might want to force an - * async behavior. This is the purpose of this function, which simply adds a - * setTimeout before resolving the deferred. - * - * @returns {Deferred} - */ - asyncWhen: function () { - var async = false; - var def = $.Deferred(); - $.when.apply($, arguments).done(function() { - var args = arguments; - var action = function() { - def.resolve.apply(def, args); - }; - if (async) { - action(); - } else { - setTimeout(action, 0); - } - }).fail(function() { - var args = arguments; - var action = function() { - def.reject.apply(def, args); - }; - if (async) { - action(); - } else { - setTimeout(action, 0); - } - }); - async = true; - return def; - }, - /** - * Returns a deferred resolved after 'wait' milliseconds + * Returns a promise resolved after 'wait' milliseconds * * @param {int} [wait=0] the delay in ms - * @return {Deferred} + * @return {Promise} */ delay: function (wait) { - var def = $.Deferred(); - setTimeout(def.resolve, wait); - return def; + return new Promise(function (resolve) { + setTimeout(resolve, wait); + }); }, /** * The DropMisordered abstraction is useful for situations where you have @@ -93,27 +54,27 @@ return { this.failMisordered = failMisordered || false; }, /** - * Adds a deferred (usually an async request) to the sequencer + * Adds a promise (usually an async request) to the sequencer * - * @param {Deferred} deferred to ensure add - * @returns {Deferred} + * @param {Promise} promise to ensure add + * @returns {Promise} */ - add: function (deferred) { - var res = $.Deferred(); - - var self = this, seq = this.lsn++; - deferred.done(function () { - if (seq > self.rsn) { - self.rsn = seq; - res.resolve.apply(res, arguments); - } else if (self.failMisordered) { - res.reject(); - } - }).fail(function () { - res.reject.apply(res, arguments); + add: function (promise) { + var self = this; + var seq = this.lsn++; + var res = new Promise(function (resolve, reject) { + promise.then(function (result) { + if (seq > self.rsn) { + self.rsn = seq; + resolve(result); + } else if (self.failMisordered) { + reject(); + } + }).guardedCatch(function (result) { + reject(result); + }); }); - - return res.promise(); + return res; }, }), /** @@ -152,17 +113,26 @@ return { */ DropPrevious: Class.extend({ /** - * Registers a new deferred and rejects the previous one + * Registers a new promise and rejects the previous one * - * @param {Deferred} deferred the new deferred + * @param {Promise} promise the new promise * @returns {Promise} */ - add: function (deferred) { - if (this.current_def) { this.current_def.reject(); } - var res = $.Deferred(); - deferred.then(res.resolve, res.reject); - this.current_def = res; - return res.promise(); + add: function (promise) { + if (this.currentDef) { + this.currentDef.reject(); + } + var rejection; + var res = new Promise(function (resolve, reject) { + rejection = reject; + promise.then(resolve).catch(function (reason) { + reject(reason); + }); + }); + + this.currentDef = res; + this.currentDef.reject = rejection; + return res; } }), /** @@ -171,7 +141,7 @@ return { * state and cause some corrupted state. * * Imagine that we have a function to fetch some data _load(), which returns - * a deferred which resolves to something useful. Now, we have some code + * a promise which resolves to something useful. Now, we have some code * looking like this:: * * return this._load().then(function (result) { @@ -200,37 +170,51 @@ return { */ Mutex: Class.extend({ init: function () { - this.def = $.Deferred().resolve(); - this.unlockedDef = undefined; + this.lock = Promise.resolve(); + this.queueSize = 0; + this.unlockedProm = undefined; + this._unlock = undefined; }, /** * Add a computation to the queue, it will be executed as soon as the * previous computations are completed. * - * @param {function} action a function which may return a deferred - * @returns {Deferred} + * @param {function} action a function which may return a Promise + * @returns {Promise} */ exec: function (action) { var self = this; - var current = this.def; - var next = this.def = $.Deferred(); - this.unlockedDef = this.unlockedDef || $.Deferred(); - return current.then(function() { - return $.when(action()).always(function () { - next.resolve(); - if (self.def.state() === 'resolved' && self.unlockedDef) { - self.unlockedDef.resolve(); - self.unlockedDef = undefined; - } + var currentLock = this.lock; + var result; + this.queueSize++; + this.unlockedProm = this.unlockedProm || new Promise(function (resolve) { + self._unlock = resolve; + }); + this.lock = new Promise(function (unlockCurrent) { + currentLock.then(function () { + result = action(); + var always = function (returnedResult) { + unlockCurrent(); + self.queueSize--; + if (self.queueSize === 0) { + self.unlockedProm = undefined; + self._unlock(); + } + return returnedResult; + }; + Promise.resolve(result).then(always).guardedCatch(always); }); }); + return this.lock.then(function () { + return result; + }); }, /** * @returns {Promise} resolved as soon as the Mutex is unlocked * (directly if it is currently idle) */ getUnlockedDef: function () { - return $.when(this.unlockedDef); + return this.unlockedProm || Promise.resolve(); }, }), /** @@ -253,65 +237,86 @@ return { * * A MutexedDropPrevious has to be a class, and not a function, because we * have to keep track of some internal state. The exec function takes as - * argument an action (and not a deferred as DropPrevious for example) + * argument an action (and not a promise as DropPrevious for example) * because it's the MutexedDropPrevious role to trigger the RPC call that - * returns a deferred when it's time. + * returns a promise when it's time. */ MutexedDropPrevious: Class.extend({ init: function () { - this.currentDef = null; this.locked = false; + this.currentProm = null; this.pendingAction = null; - this.pendingDef = null; + this.pendingProm = null; }, /** - * @param {function} action a function which may return a deferred - * @returns {Deferred} + * @param {function} action a function which may return a promise + * @returns {Promise} */ exec: function (action) { var self = this; + var resolution; + var rejection; if (this.locked) { this.pendingAction = action; - var oldPendingDef = this.pendingDef; - var pendingDef = this.pendingDef = $.Deferred(); - if (oldPendingDef) { - oldPendingDef.reject(); - } - this.currentDef.reject(); - return pendingDef.promise(); + var oldPendingDef = this.pendingProm; + + this.pendingProm = new Promise(function (resolve, reject) { + resolution = resolve; + rejection = reject; + if (oldPendingDef) { + oldPendingDef.reject(); + } + self.currentProm.reject(); + }); + this.pendingProm.resolve = resolution; + this.pendingProm.reject = rejection; + return this.pendingProm; } else { this.locked = true; - this.currentDef = $.Deferred(); - $.when(action()) - .then(this.currentDef.resolve.bind(this.currentDef)) - .fail(this.currentDef.reject.bind(this.currentDef)) - .always(function () { + this.currentProm = new Promise(function (resolve, reject) { + resolution = resolve; + rejection = reject; + function unlock() { self.locked = false; if (self.pendingAction) { var action = self.pendingAction; + var prom = self.pendingProm; self.pendingAction = null; + self.pendingProm = null; self.exec(action) - .then(self.pendingDef.resolve.bind(self.pendingDef)) - .fail(self.pendingDef.reject.bind(self.pendingDef)); + .then(prom.resolve) + .guardedCatch(prom.reject); } + } + Promise.resolve(action()) + .then(function (result) { + resolve(result); + unlock(); + }) + .guardedCatch(function (reason) { + reject(reason); + unlock(); + }); }); - return this.currentDef.promise(); + this.currentProm.resolve = resolution; + this.currentProm.reject = rejection; + return this.currentProm; } - }, + } }), /** - * Rejects a deferred as soon as a reference deferred is either resolved or + * Rejects a promise as soon as a reference promise is either resolved or * rejected * - * @param {Deferred} [target_def] the deferred to potentially reject - * @param {Deferred} [reference_def] the reference target - * @returns {Deferred} + * @param {Promise} [target_def] the promise to potentially reject + * @param {Promise} [reference_def] the reference target + * @returns {Promise} */ rejectAfter: function (target_def, reference_def) { - var res = $.Deferred(); - target_def.then(res.resolve, res.reject); - reference_def.always(res.reject); - return res.promise(); + return new Promise(function (resolve, reject) { + target_def.then(resolve).guardedCatch(reject); + reference_def.then(reject).guardedCatch(reject); + }); } }; diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index 83eded92c8a..8630b6b9364 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -52,9 +52,11 @@ var Dialog = Widget.extend({ * (use this for non-editor frontend features) */ init: function (parent, options) { + var self = this; this._super(parent); - this._opened = $.Deferred(); - + this._opened = new Promise(function (resolve) { + self._openedResolver = resolve; + }); options = _.defaults(options || {}, { title: _t('Odoo'), subtitle: '', size: 'large', @@ -140,7 +142,7 @@ var Dialog = Widget.extend({ def = buttonData.click.call(self, e); } if (buttonData.close) { - $.when(def).always(self.close.bind(self)); + Promise.resolve(def).then(self.close.bind(self)).guardedCatch(self.close.bind(self)); } }); if (self.technical) { @@ -185,7 +187,7 @@ var Dialog = Widget.extend({ self.$modal.attr('open', true); self.$modal.removeAttr("aria-hidden"); self.$modal.modal('show'); - self._opened.resolve(); + self._openedResolver(); if (options && options.shouldFocusButtons) { self._onFocusControlButton(); } diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index b8a5b8be3a4..3faa5977c8c 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -1,11 +1,10 @@ odoo.define('web.dom_ready', function (require) { 'use strict'; -var def = $.Deferred(); -$(def.resolve.bind(def)); -return def; + return new Promise(function (resolve, reject) { + $(resolve); + }); }); - //============================================================================== odoo.define('web.dom', function (require) { @@ -33,7 +32,7 @@ var _t = core._t; * that on_attach_callback() will be called on each w with arguments args */ function _notify(content, callbacks) { - _.each(callbacks, function (c) { + callbacks.forEach(function (c) { if (c.widget && c.widget.on_attach_callback) { c.widget.on_attach_callback(c.callback_args); } @@ -170,14 +169,14 @@ var dom = { * @return {jQuery} the detached elements */ detach: function (to_detach, options) { - _.each(to_detach, function (d) { + to_detach.forEach( function (d) { if (d.widget.on_detach_callback) { d.widget.on_detach_callback(d.callback_args); } }); var $to_detach = options && options.$to_detach; if (!$to_detach) { - $to_detach = $(_.map(to_detach, function (d) { + $to_detach = $(to_detach.map(function (d) { return d.widget.el; })); } @@ -229,7 +228,7 @@ var dom = { * @param {function|boolean} stopPropagation */ makeAsyncHandler: function (fct, preventDefault, stopPropagation) { - // Create a deferred indicating if a previous call to this handler is + // Create a promise indicating if a previous call to this handler is // still pending. var def = $.when(); @@ -284,11 +283,13 @@ var dom = { // a 'real' debounce creation useless. Also, during the debouncing // part, the button is disabled without any visual effect. $button.addClass('o_debounce_disabled'); - $.when(dom.DEBOUNCE && concurrency.delay(dom.DEBOUNCE)).then(function () { + Promise.resolve(dom.DEBOUNCE && concurrency.delay(dom.DEBOUNCE)).then(function () { $button.addClass('disabled').prop('disabled', true); $button.removeClass('o_debounce_disabled'); - return $.when(result).always(function () { + return Promise.resolve(result).then(function () { + $button.removeClass('disabled').prop('disabled', false); + }).guardedCatch(function () { $button.removeClass('disabled').prop('disabled', false); }); }); diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js index cd9a73ef22b..39fd8021071 100644 --- a/addons/web/static/src/js/core/misc.js +++ b/addons/web/static/src/js/core/misc.js @@ -114,7 +114,7 @@ function redirect (url, wait) { }; var wait_server = function() { - ajax.rpc("/web/webclient/version_info", {}).done(load).fail(function() { + ajax.rpc("/web/webclient/version_info", {}).then(load).guardedCatch(function () { setTimeout(wait_server, 250); }); }; @@ -172,7 +172,7 @@ core.action_registry.add("login", login); function logout() { redirect('/web/session/logout'); - return $.Deferred(); + return new Promise(); } core.action_registry.add("logout", logout); diff --git a/addons/web/static/src/js/core/mixins.js b/addons/web/static/src/js/core/mixins.js index e29c97ea086..228ac2d68dd 100644 --- a/addons/web/static/src/js/core/mixins.js +++ b/addons/web/static/src/js/core/mixins.js @@ -62,37 +62,36 @@ var ParentedMixin = { * Utility method to only execute asynchronous actions if the current * object has not been destroyed. * - * @param {$.Deferred} promise The promise representing the asynchronous + * @param {Promise} promise The promise representing the asynchronous * action. - * @param {bool} [reject=false] If true, the returned promise will be + * @param {bool} [shouldReject=false] If true, the returned promise will be * rejected with no arguments if the current * object is destroyed. If false, the * returned promise will never be resolved * or rejected. - * @returns {$.Deferred} A promise that will mirror the given promise if + * @returns {Promise} A promise that will mirror the given promise if * everything goes fine but will either be rejected * with no arguments or never resolved if the * current object is destroyed. */ - alive: function (promise, reject) { + alive: function (promise, shouldReject) { var self = this; - return $.Deferred(function (def) { - promise.then(function () { + + return new Promise(function (resolve, reject) { + promise.then(function (result) { if (!self.isDestroyed()) { - def.resolve.apply(def, arguments); + resolve(result); + } else if (shouldReject) { + reject(); } - }, function () { + }).guardedCatch(function (reason) { if (!self.isDestroyed()) { - def.reject.apply(def, arguments); - } - }).always(function () { - if (reject) { - // noop if def already resolved or rejected - def.reject(); + reject(reason); + } else if (shouldReject) { + reject(); } - // otherwise leave promise in limbo }); - }).promise(); + }); }, /** * Inform the object it should destroy itself, releasing any diff --git a/addons/web/static/src/js/core/mvc.js b/addons/web/static/src/js/core/mvc.js index cdef88c4211..6e0e4267caa 100644 --- a/addons/web/static/src/js/core/mvc.js +++ b/addons/web/static/src/js/core/mvc.js @@ -76,7 +76,7 @@ var Model = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { * @returns {Promise} The promise resolves to some kind of handle */ load: function () { - return $.when(); + return Promise.resolve(); }, }); @@ -119,9 +119,9 @@ var Controller = Widget.extend({ * @returns {Promise} */ start: function () { - return $.when( - this._super.apply(this, arguments), - this._startRenderer() + return Promise.all( + [this._super.apply(this, arguments), + this._startRenderer()] ); }, @@ -177,7 +177,8 @@ var Factory = Class.extend({ getController: function (parent) { var self = this; var model = this.getModel(parent); - return $.when(this._loadData(model), ajax.loadLibs(this)).then(function (state) { + return Promise.all([this._loadData(model), ajax.loadLibs(this)]).then(function (result) { + var state = result[0]; var renderer = self.getRenderer(parent, state); var Controller = self.Controller || self.config.Controller; var controllerParams = _.extend({ diff --git a/addons/web/static/src/js/core/rpc.js b/addons/web/static/src/js/core/rpc.js index cc56cb74ebb..21ec43e3d95 100644 --- a/addons/web/static/src/js/core/rpc.js +++ b/addons/web/static/src/js/core/rpc.js @@ -11,7 +11,7 @@ return { * * @param {Object} params @see buildQuery for a description * @param {Object} options - * @returns {Deferred<any>} + * @returns {Promise<any>} */ query: function (params, options) { var query = this.buildQuery(params); diff --git a/addons/web/static/src/js/core/service_mixins.js b/addons/web/static/src/js/core/service_mixins.js index e8088d45cb5..4f296b6bb90 100644 --- a/addons/web/static/src/js/core/service_mixins.js +++ b/addons/web/static/src/js/core/service_mixins.js @@ -124,7 +124,7 @@ var ServicesMixin = { return result; }, /** - * Builds and executes RPC query. Returns a deferred's promise resolved with + * Builds and executes RPC query. Returns a promise resolved with * the RPC result. * * @param {string} params either a route or a model @@ -133,11 +133,17 @@ var ServicesMixin = { */ _rpc: function (params, options) { var query = rpc.buildQuery(params); - var def = this.call('ajax', 'rpc', query.route, query.params, options, this) || $.Deferred(); - var promise = def.promise(); - var abort = (def.abort ? def.abort : def.reject) || function () {}; - promise.abort = abort.bind(def); - return promise; + var prom = this.call('ajax', 'rpc', query.route, query.params, options, this); + if (!prom) { + prom = new Promise(function () {}); + prom.abort = function () {}; + } + var abort = prom.abort ? prom.abort : prom.reject; + if (!abort) { + throw new Error("a rpc promise should always have a reject function"); + } + prom.abort = abort.bind(prom); + return prom; }, loadFieldView: function (modelName, context, view_id, view_type, options) { return this.loadViews(modelName, context, [[view_id, view_type]], options).then(function (result) { @@ -145,41 +151,45 @@ var ServicesMixin = { }); }, loadViews: function (modelName, context, views, options) { - var def = $.Deferred(); - this.trigger_up('load_views', { - modelName: modelName, - context: context, - views: views, - options: options, - on_success: def.resolve.bind(def), + var self = this; + return new Promise(function (resolve) { + self.trigger_up('load_views', { + modelName: modelName, + context: context, + views: views, + options: options, + on_success: resolve, + }); }); - return def; }, loadFilters: function (modelName, actionId, context) { - var def = $.Deferred(); - this.trigger_up('load_filters', { - modelName: modelName, - actionId: actionId, - context: context, - on_success: def.resolve.bind(def), + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('load_filters', { + modelName: modelName, + actionId: actionId, + context: context, + on_success: resolve, + }); }); - return def; }, createFilter: function (filter) { - var def = $.Deferred(); - this.trigger_up('create_filter', { - filter: filter, - on_success: def.resolve.bind(def), + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('create_filter', { + filter: filter, + on_success: resolve, + }); }); - return def; }, deleteFilter: function (filterId) { - var def = $.Deferred(); - this.trigger_up('delete_filter', { - filterId: filterId, - on_success: def.resolve.bind(def), + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('delete_filter', { + filterId: filterId, + on_success: resolve, + }); }); - return def; }, // Session stuff getSession: function () { @@ -195,22 +205,22 @@ var ServicesMixin = { * Informs the action manager to do an action. This supposes that the action * manager can be found amongst the ancestors of the current widget. * If that's not the case this method will simply return an unresolved - * deferred. + * promise. * * @param {any} action * @param {any} options - * @returns {Deferred} + * @returns {Promise} */ do_action: function (action, options) { - var def = $.Deferred(); - - this.trigger_up('do_action', { - action: action, - options: options, - on_success: function (result) { def.resolve(result); }, - on_fail: function (result) { def.reject(result); }, + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('do_action', { + action: action, + options: options, + on_success: resolve, + on_fail: reject, + }); }); - return def; }, do_notify: function (title, message, sticky, className) { return this.call('notification', 'notify', {title: title, message: message, sticky: sticky, className: className}); diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index 14886591d7b..2874a2ea559 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -84,27 +84,27 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { */ session_init: function () { var self = this; - var def = this.session_reload(); + var prom = this.session_reload(); if (this.is_frontend) { - return def.then(function () { + return prom.then(function () { return self.load_translations(); }); } - return def.then(function () { + return prom.then(function () { var modules = self.module_list.join(','); - var deferred = self.load_qweb(modules); + var promise = self.load_qweb(modules); if (self.session_is_valid()) { - return deferred.then(function () { return self.load_modules(); }); + return promise.then(function () { return self.load_modules(); }); } - return $.when( - deferred, + return Promise.all([ + promise, self.rpc('/web/webclient/bootstrap_translations', {mods: self.module_list}) .then(function (trans) { _t.database.set_bundle(trans); }) - ); + ]); }); }, session_is_valid: function () { @@ -119,7 +119,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { */ session_authenticate: function () { var self = this; - return $.when(this._session_authenticate.apply(this, arguments)).then(function () { + return Promise.resolve(this._session_authenticate.apply(this, arguments)).then(function () { return self.load_modules(); }); }, @@ -131,7 +131,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { var params = {db: db, login: login, password: password}; return this.rpc("/web/session/authenticate", params).then(function (result) { if (!result.uid) { - return $.Deferred().reject(); + return Promise.reject(); } _.extend(self, result); }); @@ -142,7 +142,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { }, user_has_group: function (group) { if (!this.uid) { - return $.when(false); + return Promise.resolve(false); } var def = this._groups_def[group]; if (!def) { @@ -196,22 +196,24 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { var to_load = _.difference(modules, self.module_list).join(','); this.module_list = all_modules; - var loaded = $.when(self.load_translations()); + var loaded = Promise.resolve(self.load_translations()); var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US'; var file_list = [ locale ]; if(to_load.length) { - loaded = $.when( + loaded = Promise.all([ loaded, - self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)), + self.rpc('/web/webclient/csslist', {mods: to_load}) + .then(self.load_css.bind(self)), self.load_qweb(to_load), - self.rpc('/web/webclient/jslist', {mods: to_load}).done(function (files) { - file_list = file_list.concat(files); - }) - ); + self.rpc('/web/webclient/jslist', {mods: to_load}) + .then(function (files) { + file_list = file_list.concat(files); + }) + ]); } return loaded.then(function () { return self.load_js(file_list); - }).done(function () { + }).then(function () { self.on_modules_loaded(); self.trigger('module_loaded'); }); @@ -227,24 +229,24 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { }, load_js: function (files) { var self = this; - var d = $.Deferred(); - if (files.length !== 0) { - var file = files.shift(); - var url = self.url(file, null); - ajax.loadJS(url).done(d.resolve); - } else { - d.resolve(); - } - return d; + return new Promise(function (resolve, reject) { + if (files.length !== 0) { + var file = files.shift(); + var url = self.url(file, null); + ajax.loadJS(url).then(resolve); + } else { + resolve(); + } + }); }, load_qweb: function (mods) { - this.qweb_mutex.exec(function () { + var lock = this.qweb_mutex.exec(function () { return $.get('/web/webclient/qweb?mods=' + mods).then(function (doc) { if (!doc) { return; } qweb.add_template(doc); }); }); - return this.qweb_mutex.def; + return lock; }, on_modules_loaded: function () { var openerp = window.openerp; @@ -276,12 +278,12 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { * (re)loads the content of a session: db name, username, user id, session * context and status of the support contract * - * @returns {$.Deferred} deferred indicating the session is done reloading + * @returns {Promise} promise indicating the session is done reloading */ session_reload: function () { var result = _.extend({}, window.odoo.session_info); _.extend(this, result); - return $.when(); + return Promise.resolve(); }, /** * Executes an RPC call, registering the provided callbacks. @@ -293,7 +295,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { * @param {String} url RPC endpoint * @param {Object} params call parameters * @param {Object} options additional options for rpc call - * @returns {jQuery.Deferred} jquery-provided ajax deferred + * @returns {Promise} */ rpc: function (url, params, options) { var self = this; diff --git a/addons/web/static/src/js/core/translation.js b/addons/web/static/src/js/core/translation.js index 76493fdb432..83c556c3c37 100644 --- a/addons/web/static/src/js/core/translation.js +++ b/addons/web/static/src/js/core/translation.js @@ -58,14 +58,14 @@ var TranslationDataBase = Class.extend(/** @lends instance.TranslationDataBase# @param {Object} [lang] lang The language. If not specified it will default to the language of the current user. @param {string} [url='/web/webclient/translations'] - @returns {jQuery.Deferred} + @returns {Promise} */ load_translations: function(session, modules, lang, url) { var self = this; return session.rpc(url || '/web/webclient/translations', { "mods": modules || null, "lang": lang || null - }).done(function(trans) { + }).then(function (trans) { self.set_bundle(trans); }); } diff --git a/addons/web/static/src/js/core/widget.js b/addons/web/static/src/js/core/widget.js index d3b9cc1033c..0de8c6aa1cd 100644 --- a/addons/web/static/src/js/core/widget.js +++ b/addons/web/static/src/js/core/widget.js @@ -31,7 +31,7 @@ var ServicesMixin = require('web.ServicesMixin'); * }, * willStart: function () { * // async work that need to be done before the widget is ready - * // this method should return a deferred + * // this method should return a promise * }, * start: function() { * // stuff you want to make after the rendering, `this.$el` holds a correct value @@ -135,45 +135,47 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * Method called between @see init and @see start. Performs asynchronous * calls required by the rendering and the start method. * - * This method should return a Deferred which is resolved when start can be + * This method should return a Promose which is resolved when start can be * executed. * - * @returns {Deferred} + * @returns {Promise} */ willStart: function () { - var defs = []; + var proms = []; if (this.xmlDependencies) { - defs.push.apply(defs, _.map(this.xmlDependencies, function (xmlPath) { + proms.push.apply(proms, _.map(this.xmlDependencies, function (xmlPath) { return ajax.loadXML(xmlPath, core.qweb); })); } if (this.jsLibs || this.cssLibs || this.assetLibs) { - defs.push(ajax.loadLibs(this)); + proms.push(ajax.loadLibs(this)); } - return $.when.apply($, defs); + return Promise.all(proms); }, /** * Method called after rendering. Mostly used to bind actions, perform * asynchronous calls, etc... * * By convention, this method should return an object that can be passed to - * $.when() to inform the caller when this widget has been initialized. + * Promise.resolve() to inform the caller when this widget has been initialized. * * Note that, for historic reasons, many widgets still do work in the start * method that would be more suited to the willStart method. * - * @returns {Deferred} + * @returns {Promise} */ start: function () { - return $.when(); + return Promise.resolve(); }, /** * Destroys the current widget, also destroys all its children before * destroying itself. */ destroy: function () { - _.invoke(this.getChildren(), 'destroy'); - if(this.$el) { + this.getChildren().forEach(function (child) { + child.destroy(); + }); + if (this.$el) { this.$el.remove(); } mixins.PropertiesMixin.destroy.call(this); @@ -187,6 +189,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * Renders the current widget and appends it to the given jQuery object. * * @param {jQuery} target + * @returns {Promise} */ appendTo: function (target) { var self = this; @@ -198,6 +201,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * Attach the current widget to a dom element * * @param {jQuery} target + * @returns {Promise} */ attachTo: function (target) { var self = this; @@ -234,6 +238,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * object. * * @param {jQuery} target + * @returns {Promise} */ insertAfter: function (target) { var self = this; @@ -246,6 +251,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * object. * * @param {jQuery} target + * @returns {Promise} */ insertBefore: function (target) { var self = this; @@ -257,6 +263,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * Renders the current widget and prepends it to the given jQuery object. * * @param {jQuery} target + * @returns {Promise} */ prependTo: function (target) { var self = this; @@ -282,6 +289,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * Renders the current widget and replaces the given jQuery object. * * @param target A jQuery object or a Widget instance. + * @returns {Promise} */ replace: function (target) { return this._widgetRenderAndInsert(_.bind(function (t) { @@ -415,7 +423,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * @private * @param {function: jQuery -> any} insertion * @param {jQuery} target - * @returns {Deferred} + * @returns {Promise} */ _widgetRenderAndInsert: function (insertion, target) { var self = this; |
