summaryrefslogtreecommitdiff
path: root/addons/web/static/src
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
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')
-rw-r--r--addons/web/static/src/js/_deprecated/data.js75
-rw-r--r--addons/web/static/src/js/apps.js113
-rw-r--r--addons/web/static/src/js/boot.js108
-rw-r--r--addons/web/static/src/js/chrome/abstract_action.js4
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js11
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js73
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js105
-rw-r--r--addons/web/static/src/js/chrome/action_manager_report.js57
-rw-r--r--addons/web/static/src/js/chrome/action_mixin.js8
-rw-r--r--addons/web/static/src/js/chrome/loading.js2
-rw-r--r--addons/web/static/src/js/chrome/menu.js12
-rw-r--r--addons/web/static/src/js/chrome/pager.js7
-rw-r--r--addons/web/static/src/js/chrome/sidebar.js2
-rw-r--r--addons/web/static/src/js/chrome/systray_menu.js9
-rw-r--r--addons/web/static/src/js/chrome/user_menu.js8
-rw-r--r--addons/web/static/src/js/chrome/web_client.js65
-rw-r--r--addons/web/static/src/js/core/ajax.js325
-rw-r--r--addons/web/static/src/js/core/concurrency.js239
-rw-r--r--addons/web/static/src/js/core/dialog.js10
-rw-r--r--addons/web/static/src/js/core/dom.js21
-rw-r--r--addons/web/static/src/js/core/misc.js4
-rw-r--r--addons/web/static/src/js/core/mixins.js31
-rw-r--r--addons/web/static/src/js/core/mvc.js11
-rw-r--r--addons/web/static/src/js/core/rpc.js2
-rw-r--r--addons/web/static/src/js/core/service_mixins.js92
-rw-r--r--addons/web/static/src/js/core/session.js68
-rw-r--r--addons/web/static/src/js/core/translation.js4
-rw-r--r--addons/web/static/src/js/core/widget.js34
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js49
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js23
-rw-r--r--addons/web/static/src/js/fields/field_registry.js2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js257
-rw-r--r--addons/web/static/src/js/fields/upgrade_fields.js2
-rw-r--r--addons/web/static/src/js/promise_extension.js22
-rw-r--r--addons/web/static/src/js/public/public_root.js14
-rw-r--r--addons/web/static/src/js/public/public_widget.js6
-rw-r--r--addons/web/static/src/js/report/client_action.js4
-rw-r--r--addons/web/static/src/js/services/ajax_service.js25
-rw-r--r--addons/web/static/src/js/services/core.js12
-rw-r--r--addons/web/static/src/js/services/crash_manager.js24
-rw-r--r--addons/web/static/src/js/services/data_manager.js16
-rw-r--r--addons/web/static/src/js/services/report_service.js2
-rw-r--r--addons/web/static/src/js/tools/debug_manager.js13
-rw-r--r--addons/web/static/src/js/tools/test_menus.js109
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js75
-rw-r--r--addons/web/static/src/js/views/abstract_model.js2
-rw-r--r--addons/web/static/src/js/views/abstract_renderer.js14
-rw-r--r--addons/web/static/src/js/views/abstract_view.js7
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js112
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js417
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js63
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js4
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js12
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js49
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js46
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_controller.js6
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_model.js14
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_renderer.js14
-rw-r--r--addons/web/static/src/js/views/control_panel/search/filter_menu.js4
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_bar.js19
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_bar_autocomplete_sources.js24
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_filters.js8
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js16
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js51
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js119
-rw-r--r--addons/web/static/src/js/views/form/form_view.js4
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js10
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js2
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js13
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js39
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js33
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js49
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record_quick_create.js21
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js62
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js53
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js2
-rw-r--r--addons/web/static/src/js/views/kanban/quick_create_form_view.js2
-rw-r--r--addons/web/static/src/js/views/kanban/search_panel.js30
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js49
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js122
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js92
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js23
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js2
-rw-r--r--addons/web/static/src/js/views/standalone_field_manager_mixin.js2
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js19
-rw-r--r--addons/web/static/src/js/views/view_utils.js2
-rw-r--r--addons/web/static/src/js/widgets/attach_document.js41
-rw-r--r--addons/web/static/src/js/widgets/change_password.js4
-rw-r--r--addons/web/static/src/js/widgets/data_export.js38
-rw-r--r--addons/web/static/src/js/widgets/domain_selector.js25
-rw-r--r--addons/web/static/src/js/widgets/domain_selector_dialog.js4
-rw-r--r--addons/web/static/src/js/widgets/iframe_widget.js2
-rw-r--r--addons/web/static/src/js/widgets/model_field_selector.js24
-rw-r--r--addons/web/static/src/js/widgets/name_and_signature.js10
-rw-r--r--addons/web/static/src/js/widgets/pie_chart.js2
-rw-r--r--addons/web/static/src/js/widgets/switch_company_menu.js12
96 files changed, 2092 insertions, 1857 deletions
diff --git a/addons/web/static/src/js/_deprecated/data.js b/addons/web/static/src/js/_deprecated/data.js
index 984f0bbaddb..acac959fcc8 100644
--- a/addons/web/static/src/js/_deprecated/data.js
+++ b/addons/web/static/src/js/_deprecated/data.js
@@ -101,7 +101,7 @@ var Query = Class.extend({
* Fetches the first record matching the query, or null
*
* @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<Object|null>}
+ * @returns {Promise<Object|null>}
*/
first: function (options) {
var self = this;
@@ -115,7 +115,7 @@ var Query = Class.extend({
* Fetches all records matching the query
*
* @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<Array<>>}
+ * @returns {Promise<Array<>>}
*/
all: function (options) {
return this._execute(options);
@@ -123,10 +123,10 @@ var Query = Class.extend({
/**
* Fetches the number of records matching the query in the database
*
- * @returns {jQuery.Deferred<Number>}
+ * @returns {Promise<Number>}
*/
count: function () {
- if (this._count !== undefined) { return $.when(this._count); }
+ if (this._count !== undefined) { return Promise.resolve(this._count); }
return this._model.call(
'search_count', [this._filter], {
context: this._model.context(this._context)});
@@ -367,11 +367,12 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @param {Array} ids identifiers of the records to read
* @param {Array} [fields] fields to read and return, by default all fields are returned
* @param {Object} [options]
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
read_ids: function (ids, fields, options) {
- if (_.isEmpty(ids))
- return $.Deferred().resolve([]);
+ if (_.isEmpty(ids)) {
+ return Promise.resolve([]);
+ }
options = options || {};
var method = 'read';
@@ -405,7 +406,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @params {Object} [options]
* @param {Number} [options.offset=0] The index from which selected records should be returned
* @param {Number} [options.limit=null] The maximum number of records to return
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
read_slice: function (fields, options) {
var self = this;
@@ -414,21 +415,23 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
.limit(options.limit || false)
.offset(options.offset || 0)
.all();
- return this.orderer.add(query).done(function (records) {
+ var prom = this.orderer.add(query);
+ prom.then(function (records) {
self.ids = _(records).pluck('id');
});
+ return prom;
},
/**
* Reads the current dataset record (from its index)
*
* @params {Array} [fields] fields to read and return, by default all fields are returned
* @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
read_index: function (fields, options) {
options = options || {};
return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
- if (_.isEmpty(records)) { return $.Deferred().reject().promise(); }
+ if (_.isEmpty(records)) { return Promise.reject(); }
return records[0];
});
},
@@ -437,7 +440,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
*
* @param {Array} [fields] fields to get default values for, by default all defaults are read
* @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
default_get: function (fields, options) {
options = options || {};
@@ -451,15 +454,17 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @param {Object} options Dictionary that can contain the following keys:
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
create: function (data, options) {
var self = this;
- return this._model.call('create', [data], {
+ var prom = this._model.call('create', [data], {
context: this.get_context()
- }).done(function () {
+ });
+ prom.then(function () {
self.trigger('dataset_changed', data, options);
});
+ return prom;
},
/**
* Saves the provided data in an existing db record
@@ -470,16 +475,18 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* - context: The context to use in the server-side call.
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
write: function (id, data, options) {
options = options || {};
var self = this;
- return this._model.call('write', [[id], data], {
+ var prom = this._model.call('write', [[id], data], {
context: this.get_context(options.context)
- }).done(function () {
+ });
+ prom.then(function () {
self.trigger('dataset_changed', id, data, options);
});
+ return prom;
},
/**
* Deletes an existing record from the database
@@ -488,11 +495,13 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
*/
unlink: function (ids) {
var self = this;
- return this._model.call('unlink', [ids], {
+ var prom = this._model.call('unlink', [ids], {
context: this.get_context()
- }).done(function () {
+ });
+ prom.then(function () {
self.trigger('dataset_changed', ids);
});
+ return prom;
},
/**
* Calls an arbitrary RPC method
@@ -501,7 +510,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @param {Array} [args] arguments to pass to the method
* @param {Function} callback
* @param {Function} error_callback
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
call: function (method, args) {
return this._model.call(method, args);
@@ -511,7 +520,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
*
* @param {String} method
* @param {Array} [args]
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
call_button: function (method, args) {
return this._model.call_button(method, args);
@@ -520,7 +529,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* Fetches the "readable name" for records, based on intrinsic rules
*
* @param {Array} ids
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
name_get: function (ids) {
return this._model.call('name_get', [ids], {context: this.get_context()});
@@ -532,7 +541,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @param {String} [operator='ilike'] matching operator to use with the provided name value
* @param {Number} [limit=0] maximum number of matches to return
* @param {Function} callback function to call with name_search result
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
name_search: function (name, domain, operator, limit) {
return this._model.call('name_search', {
@@ -603,7 +612,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* Resequence records.
*
* @param {Array} ids identifiers of the records to resequence
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
resequence: function (ids, options) {
options = options || {};
@@ -643,7 +652,7 @@ var DataSetStatic = DataSet.extend({
unlink: function (ids) {
this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
this.trigger('unlink', ids);
- return $.Deferred().resolve({result: true});
+ return Promise.resolve({result: true});
},
});
@@ -674,7 +683,7 @@ var DataSetSearch = DataSet.extend({
* @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
* @param {Number} [options.offset=0] The index from which selected records should be returned
* @param {Number} [options.limit=null] The maximum number of records to return
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
read_slice: function (fields, options) {
options = options || {};
@@ -686,11 +695,13 @@ var DataSetSearch = DataSet.extend({
.limit(options.limit || false);
q = q.order_by.apply(q, this._sort);
- return this.orderer.add(q.all()).done(function (records) {
+ var prom = this.orderer.add(q.all());
+ prom.then(function (records) {
// FIXME: not sure about that one, *could* have discarded count
- q.count().done(function (count) { self._length = count; });
+ q.count().then(function (count) { self._length = count; });
self.ids = _(records).pluck('id');
});
+ return prom;
},
get_domain: function (other_domain) {
return this._model.domain(other_domain);
@@ -717,10 +728,12 @@ var DataSetSearch = DataSet.extend({
},
unlink: function (ids, callback, error_callback) {
var self = this;
- return this._super(ids).done(function () {
+ var prom = this._super(ids);
+ prom.then(function () {
self.remove_ids( ids);
self.trigger("dataset_changed", ids, callback, error_callback);
});
+ return prom;
},
size: function () {
if (this._length !== null) {
@@ -801,7 +814,7 @@ var Model = Class.extend({
* @param {Array} [args] positipyEvalonal arguments
* @param {Object} [kwargs] keyword arguments
* @param {Object} [options] additional options for the rpc() method
- * @returns {jQuery.Deferred<>} call result
+ * @returns {Promise<>} call result
*/
call: function (method, args, kwargs, options) {
args = args || [];
diff --git a/addons/web/static/src/js/apps.js b/addons/web/static/src/js/apps.js
index aadcc700984..739317337ba 100644
--- a/addons/web/static/src/js/apps.js
+++ b/addons/web/static/src/js/apps.js
@@ -22,20 +22,21 @@ var Apps = AbstractAction.extend({
},
get_client: function() {
- // return the client via a deferred, resolved or rejected depending if
+ // return the client via a promise, resolved or rejected depending if
// the remote host is available or not.
var check_client_available = function(client) {
- var d = $.Deferred();
var i = new Image();
- i.onerror = function() {
- d.reject(client);
- };
- i.onload = function() {
- d.resolve(client);
- };
+ var def = new Promise(function (resolve, reject) {
+ i.onerror = function() {
+ reject(client);
+ };
+ i.onload = function() {
+ resolve(client);
+ };
+ });
var ts = new Date().getTime();
i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif?%s', client.origin, ts);
- return d.promise();
+ return def;
};
if (apps_client) {
return check_client_available(apps_client);
@@ -105,58 +106,56 @@ var Apps = AbstractAction.extend({
start: function() {
var self = this;
- var def = $.Deferred();
- self.get_client().then(function(client) {
- self.client = client;
-
- var qs = {db: client.dbname};
- if (session.debug) {
- qs.debug = session.debug;
- }
- var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
- var css = {width: '100%', height: '750px'};
- self.$ifr = $('<iframe>').attr('src', u);
-
- self.uniq = _.uniqueId('apps');
- $(window).on("message." + self.uniq, self.proxy('_on_message'));
-
- self.on('message:ready', self, function(m) {
- var w = this.$ifr[0].contentWindow;
- var act = {
- type: 'ir.actions.client',
- tag: this.remote_action_tag,
- params: _.extend({}, this.params, {
- db: session.db,
- origin: session.origin,
- })
- };
- w.postMessage({type:'action', action: act}, client.origin);
- });
+ return new Promise(function (resolve, reject) {
+ self.get_client().then(function (client) {
+ self.client = client;
+
+ var qs = {db: client.dbname};
+ if (session.debug) {
+ qs.debug = session.debug;
+ }
+ var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
+ var css = {width: '100%', height: '750px'};
+ self.$ifr = $('<iframe>').attr('src', u);
+
+ self.uniq = _.uniqueId('apps');
+ $(window).on("message." + self.uniq, self.proxy('_on_message'));
+
+ self.on('message:ready', self, function(m) {
+ var w = this.$ifr[0].contentWindow;
+ var act = {
+ type: 'ir.actions.client',
+ tag: this.remote_action_tag,
+ params: _.extend({}, this.params, {
+ db: session.db,
+ origin: session.origin,
+ })
+ };
+ w.postMessage({type:'action', action: act}, client.origin);
+ });
- self.on('message:set_height', self, function(m) {
- this.$ifr.height(m.height);
- });
+ self.on('message:set_height', self, function(m) {
+ this.$ifr.height(m.height);
+ });
- self.on('message:blockUI', self, function() { framework.blockUI(); });
- self.on('message:unblockUI', self, function() { framework.unblockUI(); });
- self.on('message:warn', self, function(m) {self.do_warn(m.title, m.message, m.sticky); });
-
- self.$ifr.appendTo(self.$('.o_content')).css(css).addClass('apps-client');
-
- def.resolve();
- }, function() {
- self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
- return self._rpc({
- route: '/web/action/load',
- params: {action_id: self.failback_action_id},
- }).then(function(action) {
- return self.do_action(action);
- }).always(function () {
- def.reject();
+ self.on('message:blockUI', self, function() { framework.blockUI(); });
+ self.on('message:unblockUI', self, function() { framework.unblockUI(); });
+ self.on('message:warn', self, function(m) {self.do_warn(m.title, m.message, m.sticky); });
+
+ self.$ifr.appendTo(self.$('.o_content')).css(css).addClass('apps-client');
+
+ resolve();
+ }, function() {
+ self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
+ return self._rpc({
+ route: '/web/action/load',
+ params: {action_id: self.failback_action_id},
+ }).then(function(action) {
+ return self.do_action(action);
+ }).then(reject, reject);
});
});
- return def;
- },
+ }
});
var AppsUpdates = Apps.extend({
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
index 994b945b1d3..cf1cbaba180 100644
--- a/addons/web/static/src/js/boot.js
+++ b/addons/web/static/src/js/boot.js
@@ -6,8 +6,8 @@
* @name openerp
* @namespace openerp
*
- * Each module can return a deferred. In that case, the module is marked as loaded
- * only when the deferred is resolved, and its value is equal to the resolved value.
+ * Each module can return a promise. In that case, the module is marked as loaded
+ * only when the promise is resolved, and its value is equal to the resolved value.
* The module can be rejected (unloaded). This will be logged in the console as info.
*
* logs:
@@ -17,7 +17,7 @@
* Failed modules:
* A javascript error is detected
* Rejected modules:
- * The module returns a rejected deferred. It (and its dependent modules)
+ * The module returns a rejected promise. It (and its dependent modules)
* is not loaded.
* Rejected linked modules:
* Modules who depend on a rejected module
@@ -27,14 +27,14 @@
* Non loaded or failed module informations for debugging
*/
-(function() {
+(function () {
"use strict";
var jobs = [];
var factories = Object.create(null);
var job_names = [];
var job_deps = [];
- var job_deferred = [];
+ var job_promise = [];
var services = Object.create({});
@@ -45,15 +45,20 @@
var debug = ($.deparam($.param.querystring()).debug !== undefined);
var odoo = window.odoo = window.odoo || {};
+ var didLogInfoResolve;
+ var didLogInfoPromise = new Promise(function (resolve) {
+ didLogInfoResolve = resolve;
+ });
+
_.extend(odoo, {
testing: typeof QUnit === "object",
debug: debug,
remaining_jobs: jobs,
__DEBUG__: {
- didLogInfo: $.Deferred(),
+ didLogInfo: didLogInfoPromise,
get_dependencies: function (name, transitive) {
- var deps = name instanceof Array ? name: [name],
+ var deps = name instanceof Array ? name : [name],
changed;
do {
changed = false;
@@ -67,26 +72,26 @@
return deps;
},
get_dependents: function (name) {
- return _.pluck(_.where(job_deps, {from: name}), 'to');
+ return _.pluck(_.where(job_deps, { from: name }), 'to');
},
get_waited_jobs: function () {
- return _.uniq(_.map(jobs, function(job) {return job.name;}));
+ return _.uniq(_.map(jobs, function (job) { return job.name; }));
},
get_missing_jobs: function () {
var self = this;
var waited = this.get_waited_jobs();
var missing = [];
- _.each(waited, function(job) {
- _.each(self.get_dependencies(job), function(job) {
+ _.each(waited, function (job) {
+ _.each(self.get_dependencies(job), function (job) {
if (!(job in self.services)) {
missing.push(job);
}
});
});
- return _.filter(_.difference(_.uniq(missing), waited), function (job) {return !job.error;});
+ return _.filter(_.difference(_.uniq(missing), waited), function (job) { return !job.error; });
},
get_failed_jobs: function () {
- return _.filter(jobs, function (job) {return !!job.error;});
+ return _.filter(jobs, function (job) { return !!job.error; });
},
factories: factories,
services: services,
@@ -109,10 +114,10 @@
if (odoo.debug) {
if (!(deps instanceof Array)) {
- throw new Error ('Dependencies should be defined by an array', deps);
+ throw new Error('Dependencies should be defined by an array', deps);
}
if (typeof factory !== 'function') {
- throw new Error ('Factory should be defined by a function', factory);
+ throw new Error('Factory should be defined by a function', factory);
}
if (typeof name !== 'string') {
throw new Error("Invalid name definition (should be a string", name);
@@ -133,7 +138,7 @@
job_names.push(name);
_.each(deps, function (dep) {
- job_deps.push({from:dep, to:name});
+ job_deps.push({ from: dep, to: name });
});
this.process_jobs(jobs, services);
@@ -149,7 +154,7 @@
var job;
var jobdep;
- for (var k=0; k<jobs.length; k++) {
+ for (var k = 0; k < jobs.length; k++) {
debug_jobs[jobs[k].name] = job = {
dependencies: jobs[k].deps,
dependents: odoo.__DEBUG__.get_dependents(jobs[k].name),
@@ -162,10 +167,10 @@
job.rejected = jobs[k].rejected;
rejected.push(job.name);
}
- var deps = odoo.__DEBUG__.get_dependencies( job.name );
- for (var i=0; i<deps.length; i++) {
+ var deps = odoo.__DEBUG__.get_dependencies(job.name);
+ for (var i = 0; i < deps.length; i++) {
if (job.name !== deps[i] && !(deps[i] in services)) {
- jobdep = debug_jobs[deps[i]] || (deps[i] in factories && _.find(jobs, function (job) { return job.name === deps[i];}));
+ jobdep = debug_jobs[deps[i]] || (deps[i] in factories && _.find(jobs, function (job) { return job.name === deps[i]; }));
if (jobdep && jobdep.rejected) {
if (!job.rejected) {
job.rejected = [];
@@ -212,48 +217,49 @@
missing: missing,
failed: _.pluck(failed, 'name'),
};
- odoo.__DEBUG__.didLogInfo.resolve();
+ didLogInfoResolve();
},
process_jobs: function (jobs, services) {
var job;
- function process_job (job) {
+ function process_job(job) {
var require = make_require(job);
var job_exec;
- var def = $.Deferred();
- try {
- job_exec = job.factory.call(null, require);
- jobs.splice(jobs.indexOf(job), 1);
- job_deferred.push(def);
- } catch (e) {
- job.error = e;
- console.error('Error while loading ' + job.name);
- console.error(e.stack);
- }
- if (!job.error) {
- $.when(job_exec).then(
- function (data) {
- services[job.name] = data;
- def.resolve();
- odoo.process_jobs(jobs, services);
- }, function (e) {
- job.rejected = e || true;
- jobs.push(job);
- def.resolve();
- }
- );
- }
+ var def = new Promise(function (resolve) {
+ try {
+ job_exec = job.factory.call(null, require);
+ jobs.splice(jobs.indexOf(job), 1);
+ } catch (e) {
+ job.error = e;
+ console.error('Error while loading ' + job.name);
+ console.error(e.stack);
+ }
+ if (!job.error) {
+ Promise.resolve(job_exec).then(
+ function (data) {
+ services[job.name] = data;
+ resolve();
+ odoo.process_jobs(jobs, services);
+ }).guardedCatch(function (e) {
+ job.rejected = e || true;
+ jobs.push(job);
+ resolve();
+ }
+ );
+ }
+ });
+ job_promise.push(def);
}
- function is_ready (job) {
+ function is_ready(job) {
return !job.error && !job.rejected && _.every(job.factory.deps, function (name) { return name in services; });
}
- function make_require (job) {
+ function make_require(job) {
var deps = _.pick(services, job.deps);
- function require (name) {
+ function require(name) {
if (!(name in deps)) {
console.error('Undefined dependency: ', name);
} else {
@@ -278,9 +284,9 @@
// automatically log errors detected when loading modules
var log_when_loaded = function () {
_.delay(function () {
- var len = job_deferred.length;
- $.when.apply($, job_deferred).then(function () {
- if (len === job_deferred.length) {
+ var len = job_promise.length;
+ Promise.all(job_promise).then(function () {
+ if (len === job_promise.length) {
odoo.log();
} else {
log_when_loaded();
diff --git a/addons/web/static/src/js/chrome/abstract_action.js b/addons/web/static/src/js/chrome/abstract_action.js
index a5f98434ddb..1b258ccc6f6 100644
--- a/addons/web/static/src/js/chrome/abstract_action.js
+++ b/addons/web/static/src/js/chrome/abstract_action.js
@@ -104,7 +104,7 @@ var AbstractAction = Widget.extend(ActionMixin, {
};
});
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
var controlPanelView = new self.config.ControlPanelView(params);
return controlPanelView.getController(self).then(function (controlPanel) {
self._controlPanel = controlPanel;
@@ -112,7 +112,7 @@ var AbstractAction = Widget.extend(ActionMixin, {
});
});
}
- return $.when();
+ return Promise.resolve();
},
/**
* @override
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 7082f26e69d..68ac157868d 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -78,7 +78,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
if (event.data.on_success) {
event.data.on_success(result);
}
- }).fail(function (result) {
+ }).guardedCatch(function (result) {
if (event.data.on_fail) {
event.data.on_fail(result);
}
@@ -116,17 +116,17 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
.then(function () {
self.$el.toggleClass('o_rtl', _t.database.parameters.direction === "rtl");
self.bind_events();
- return $.when(
+ return Promise.all([
self.set_action_manager(),
self.set_loading()
- );
+ ]);
}).then(function () {
if (session.session_is_valid()) {
return self.show_application();
} else {
// database manager needs the webclient to keep going even
// though it has no valid session
- return $.when();
+ return Promise.resolve();
}
}).then(function () {
// Listen to 'scroll' event and propagate it on main bus
@@ -145,12 +145,13 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function () {
$(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
});
+ // TODO: this handler seems useless since 11.0, should be removed
this.$el.on('click', '.oe_dropdown_toggle', function (ev) {
ev.preventDefault();
var $toggle = $(this);
var doc_width = $(document).width();
var $menu = $toggle.siblings('.oe_dropdown_menu');
- $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
+ $menu = $menu.length >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
var state = $menu.is('.oe_opened');
setTimeout(function () {
// Do not alter propagation
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 107c6e314eb..0537b45deb5 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -92,7 +92,7 @@ var ActionManager = Widget.extend({
* will be restored. It ensures that the current controller can be left (for
* instance, that it has no unsaved changes).
*
- * @returns {Deferred} resolved if the current controller can be left,
+ * @returns {Promise} resolved if the current controller can be left,
* rejected otherwise.
*/
clearUncommittedChanges: function () {
@@ -100,7 +100,7 @@ var ActionManager = Widget.extend({
if (currentController) {
return currentController.widget.canBeRemoved();
}
- return $.when();
+ return Promise.resolve();
},
/**
* This is the entry point to execute Odoo actions, given as an ID in
@@ -123,7 +123,7 @@ var ActionManager = Widget.extend({
* is useful when we come from a loadState())
* @param {boolean} [options.replace_last_action=false] set to true to
* replace last part of the breadcrumbs with the action
- * @return {$.Deferred<Object>} resolved with the action when the action is
+ * @return {Promise<Object>} resolved with the action when the action is
* loaded and appended to the DOM ; rejected if the action can't be
* executed (e.g. if doAction has been called to execute another action
* before this one was complete).
@@ -155,7 +155,7 @@ var ActionManager = Widget.extend({
});
}
- return this.dp.add($.when(def)).then(function () {
+ return this.dp.add(Promise.resolve(def)).then(function () {
// action.target 'main' is equivalent to 'current' except that it
// also clears the breadcrumbs
options.clear_breadcrumbs = action.target === 'main' ||
@@ -211,12 +211,12 @@ var ActionManager = Widget.extend({
* @param {Object} state
* @param {integer|string} [state.action] the action to execute (given its
* id or tag for client actions)
- * @returns {Deferred} resolved when the UI has been updated
+ * @returns {Promise} resolved when the UI has been updated
*/
loadState: function (state) {
var action;
if (!state.action) {
- return $.when();
+ return Promise.resolve();
}
if (_.isString(state.action) && core.action_registry.contains(state.action)) {
action = {
@@ -299,7 +299,7 @@ var ActionManager = Widget.extend({
* @param {string} [action.target="current"] set to "new" to render the
* controller in a dialog
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the controller is started and appended
+ * @returns {Promise} resolved when the controller is started and appended
*/
_executeAction: function (action, options) {
var self = this;
@@ -338,7 +338,7 @@ var ActionManager = Widget.extend({
return action;
})
- .fail(function () {
+ .guardedCatch(function () {
self._removeAction(action.jsID);
});
},
@@ -349,7 +349,7 @@ var ActionManager = Widget.extend({
* @private
* @param {Object} action
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the controller is rendered inside a
+ * @returns {Promise} resolved when the controller is rendered inside a
* dialog appended to the DOM
*/
_executeActionInDialog: function (action, options) {
@@ -401,7 +401,7 @@ var ActionManager = Widget.extend({
return action;
});
- }).fail(function () {
+ }).guardedCatch(function () {
self._removeAction(action.jsID);
});
},
@@ -412,14 +412,14 @@ var ActionManager = Widget.extend({
* @param {Object} action the description of the action to execute
* @param {string} action.tag the key of the action in the action_registry
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the client action has been executed
+ * @returns {Promise} resolved when the client action has been executed
*/
_executeClientAction: function (action, options) {
var self = this;
var ClientAction = core.action_registry.get(action.tag);
if (!ClientAction) {
console.error("Could not find client action " + action.tag, action);
- return $.Deferred().reject();
+ return Promise.reject();
}
if (!(ClientAction.prototype instanceof AbstractAction)) {
console.warn('The client action ' + action.tag + ' should be an instance of AbstractAction!');
@@ -431,7 +431,7 @@ var ActionManager = Widget.extend({
if (next) {
return this.doAction(next, options);
}
- return $.when();
+ return Promise.resolve();
}
var controllerID = _.uniqueId('controller_');
@@ -449,12 +449,14 @@ var ActionManager = Widget.extend({
};
this.controllers[controllerID] = controller;
action.controllerID = controllerID;
- return this._executeAction(action, options).done(function () {
+ var prom = this._executeAction(action, options);
+ prom.then(function () {
// AAB: this should be done automatically in AbstractAction, so that
// it can be overriden by actions that have specific stuff to push
// (e.g. Discuss, Views)
self._pushState(controllerID, {});
});
+ return prom;
},
/**
* Executes actions of type 'ir.actions.act_window_close', i.e. closes the
@@ -469,7 +471,7 @@ var ActionManager = Widget.extend({
* @param {Object} [action.effect] effect to show up, e.g. rainbow man.
* @param {Object} [action.infos] infos on performing the close action.
* Useful for providing some context for the `on_close` handler.
- * @returns {Deferred} resolved immediately
+ * @returns {Promise} resolved immediately
*/
_executeCloseAction: function (action, options) {
var result;
@@ -484,7 +486,7 @@ var ActionManager = Widget.extend({
this.trigger_up('show_effect', action.effect);
}
- return $.when(result);
+ return Promise.resolve(result);
},
/**
* Executes actions of type 'ir.actions.server'.
@@ -494,7 +496,7 @@ var ActionManager = Widget.extend({
* @param {integer} action.id the db ID of the action to execute
* @param {Object} [action.context]
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the action has been executed
+ * @returns {Promise} resolved when the action has been executed
*/
_executeServerAction: function (action, options) {
var self = this;
@@ -520,7 +522,7 @@ var ActionManager = Widget.extend({
* @param {string} [action.target] set to 'self' to redirect in the current page,
* redirects to a new page by default
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the redirection is done (immediately
+ * @returns {Promise} resolved when the redirection is done (immediately
* when redirecting to a new page)
*/
_executeURLAction: function (action, options) {
@@ -531,7 +533,7 @@ var ActionManager = Widget.extend({
if (action.target === 'self') {
framework.redirect(url);
- return $.Deferred();
+ return Promise.resolve();
} else {
var w = window.open(url, '_blank');
if (!w || w.closed || typeof w.closed === 'undefined') {
@@ -544,7 +546,7 @@ var ActionManager = Widget.extend({
options.on_close();
- return $.when();
+ return Promise.resolve();
},
/**
* Returns a description of the controllers in the given controller stack.
@@ -653,14 +655,14 @@ var ActionManager = Widget.extend({
* @param {Object} action
* @param {string} action.type
* @param {Object} options
- * @returns {Deferred} resolved when the action has been executed ; rejected
+ * @returns {Promise} resolved when the action has been executed ; rejected
* if the type of action isn't supported, or if the action can't be
* executed
*/
_handleAction: function (action, options) {
if (!action.type) {
console.error("No type for action", action);
- return $.Deferred().reject();
+ return Promise.reject();
}
switch (action.type) {
case 'ir.actions.act_url':
@@ -674,7 +676,7 @@ var ActionManager = Widget.extend({
default:
console.error("The ActionManager can't handle actions of type " +
action.type, action);
- return $.Deferred().reject();
+ return Promise.reject();
}
},
/**
@@ -749,16 +751,17 @@ var ActionManager = Widget.extend({
* @private
* @param {integer|string} action's ID or xml ID
* @param {Object} context
- * @returns {Deferred<Object>} resolved with the description of the action
+ * @returns {Promise<Object>} resolved with the description of the action
*/
_loadAction: function (actionID, context) {
- var def = $.Deferred();
- this.trigger_up('load_action', {
- actionID: actionID,
- context: context,
- on_success: def.resolve.bind(def),
+ var self = this;
+ return new Promise(function (resolve, reject) {
+ self.trigger_up('load_action', {
+ actionID: actionID,
+ context: context,
+ on_success: resolve,
+ });
});
- return def;
},
/**
* Preprocesses the action before it is handled by the ActionManager
@@ -815,20 +818,20 @@ var ActionManager = Widget.extend({
*
* @private
* @param {string} controllerID
- * @returns {Deferred} resolved when the controller has been restored
+ * @returns {Promise} resolved when the controller has been restored
*/
_restoreController: function (controllerID) {
var self = this;
var controller = this.controllers[controllerID];
// AAB: AbstractAction should define a proper hook to execute code when
- // it is restored (other than do_show), and it should return a deferred
+ // it is restored (other than do_show), and it should return a promise
var action = this.actions[controller.actionID];
var def;
if (action.on_reverse_breadcrumb) {
def = action.on_reverse_breadcrumb();
}
- return $.when(def).then(function () {
- return $.when(controller.widget.do_show()).then(function () {
+ return Promise.resolve(def).then(function () {
+ return Promise.resolve(controller.widget.do_show()).then(function () {
var index = _.indexOf(self.controllerStack, controllerID);
self._pushController(controller, index);
});
@@ -844,7 +847,7 @@ var ActionManager = Widget.extend({
*
* @private
* @param {Object} controller
- * @returns {Deferred<Object>} resolved with the controller when it is ready
+ * @returns {Promise<Object>} resolved with the controller when it is ready
*/
_startController: function (controller) {
var fragment = document.createDocumentFragment();
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index 106f9f2aa47..d00b3a49eb2 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -144,7 +144,7 @@ ActionManager.include({
* @param {integer} [options.index=0] the controller's index in the stack
* @param {boolean} [options.lazy=false] set to true to differ the
* initialization of the controller's widget
- * @returns {Deferred<Object>} resolved with the created controller
+ * @returns {Promise<Object>} resolved with the created controller
*/
_createViewController: function (action, viewType, viewOptions, options) {
var self = this;
@@ -153,7 +153,7 @@ ActionManager.include({
// the requested view type isn't specified in the action (e.g.
// action with list view only, user clicks on a row in the list, it
// tries to switch to form view)
- return $.Deferred().reject();
+ return Promise.reject();
}
options = options || {};
@@ -186,26 +186,31 @@ ActionManager.include({
// communication with trigger_up
controllerID: controllerID,
});
+ var rejection;
var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
- var def = $.Deferred();
- action.controllers[viewType] = def;
- view.getController(this).then(function (widget) {
- if (def.state() === 'rejected') {
- // the deferred has been rejected meanwhile, meaning that
- // the action has been removed, so simply destroy the widget
- widget.destroy();
- } else {
- controller.widget = widget;
- def.resolve(controller);
- }
- }).fail(def.reject.bind(def));
- def.fail(function () {
+ var def = new Promise(function (resolve, reject) {
+ rejection = reject;
+ view.getController(self).then(function (widget) {
+ if (def.rejected) {
+ // the promise has been rejected meanwhile, meaning that
+ // the action has been removed, so simply destroy the widget
+ widget.destroy();
+ } else {
+ controller.widget = widget;
+ resolve(controller);
+ }
+ }).guardedCatch(reject);
+ });
+ // Need to define an reject property to call it into _destroyWindowAction
+ def.reject = rejection;
+ def.guardedCatch(function () {
+ def.rejected = true;
delete self.controllers[controllerID];
});
+ action.controllers[viewType] = def;
} else {
- action.controllers[viewType] = $.Deferred().resolve(controller);
+ action.controllers[viewType] = Promise.resolve(controller);
}
-
return action.controllers[viewType];
},
/**
@@ -217,18 +222,22 @@ ActionManager.include({
*/
_destroyWindowAction: function (action) {
var self = this;
- _.each(action.controllers, function (controllerDef) {
+ for (var c in action.controllers) {
+ var controllerDef = action.controllers[c];
controllerDef.then(function (controller) {
delete self.controllers[controller.jsID];
if (controller.widget) {
controller.widget.destroy();
}
});
- // reject the deferred if it is not yet resolved, so that the
- // controller is correctly destroyed as soon as it is ready, and
- // its reference is removed
- controllerDef.reject();
- });
+ // If controllerDef is not resolved yet, reject it so that the
+ // controller will be correctly destroyed as soon as it'll be ready,
+ // and its reference will be removed. Lazy-loaded controllers do
+ // not have a reject function on their promise
+ if (controllerDef.reject) {
+ controllerDef.reject();
+ }
+ }
},
/**
* Executes actions of type 'ir.actions.act_window'.
@@ -239,13 +248,13 @@ ActionManager.include({
* @param {Object} options @see doAction for details
* @param {integer} [options.resID] the current res ID
* @param {string} [options.viewType] the view to open
- * @returns {Deferred} resolved when the action is appended to the DOM
+ * @returns {Promise} resolved when the action is appended to the DOM
*/
_executeWindowAction: function (action, options) {
var self = this;
return this.dp.add(this._loadViews(action)).then(function (fieldsViews) {
var views = self._generateActionViews(action, fieldsViews);
- action._views = action.views; // save the initial attribute
+ action._views = action.views; // save the initial attribute
action.views = views;
action.controlPanelFieldsView = fieldsViews.search;
action.controllers = {};
@@ -285,7 +294,7 @@ ActionManager.include({
self.controllerStack.push(lazyLoadedController.jsID);
});
}
- return self.dp.add($.when(lazyViewDef))
+ return self.dp.add(Promise.resolve(lazyViewDef))
.then(function () {
var viewOptions = {
controllerState: options.controllerState,
@@ -300,7 +309,7 @@ ActionManager.include({
action.controllerID = controller.jsID;
return self._executeAction(action, options);
})
- .fail(self._destroyWindowAction.bind(self, action));
+ .guardedCatch(self._destroyWindowAction.bind(self, action));
});
},
/**
@@ -389,7 +398,7 @@ ActionManager.include({
*
* @private
* @param {Object} action
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadViews: function (action) {
var inDialog = action.target === 'new';
@@ -443,11 +452,11 @@ ActionManager.include({
return this.clearUncommittedChanges().then(function () {
// AAB: this will be done directly in AbstractAction's restore
// function
- var def;
+ var def = Promise.resolve();
if (action.on_reverse_breadcrumb) {
def = action.on_reverse_breadcrumb();
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
return self._switchController(action, controller.viewType);
});
});
@@ -461,14 +470,14 @@ ActionManager.include({
* @private
* @param {Object} controller the controller to switch to
* @param {Object} [viewOptions]
- * @return {Deferred} resolved when the new controller is in the DOM
+ * @return {Promise} resolved when the new controller is in the DOM
*/
_switchController: function (action, viewType, viewOptions) {
var self = this;
var view = _.findWhere(action.views, {type: viewType});
if (!view) {
// can't switch to an unknown view
- return $.Deferred().reject();
+ return Promise.reject();
}
var currentController = this.getCurrentController();
var index;
@@ -519,28 +528,30 @@ ActionManager.include({
};
var controllerDef = action.controllers[viewType];
- if (!controllerDef || controllerDef.state() === 'rejected') {
- // if the controllerDef is rejected, it probably means that the js
- // code or the requests made to the server crashed. In that case,
- // if we reuse the same deferred, then the switch to the view is
- // definitely blocked. We want to use a new controller, even though
- // it is very likely that it will recrash again. At least, it will
- // give more feedback to the user, and it could happen that one
- // record crashes, but not another.
- controllerDef = newController();
- } else {
+ if (controllerDef) {
controllerDef = controllerDef.then(function (controller) {
if (!controller.widget) {
// lazy loaded -> load it now (with same jsID)
return newController(controller.jsID);
} else {
- return $.when(controller.widget.willRestore()).then(function () {
+ return Promise.resolve(controller.widget.willRestore()).then(function () {
return controller.widget.reload(viewOptions).then(function () {
return controller;
});
});
}
+ }, function () {
+ // if the controllerDef is rejected, it probably means that the js
+ // code or the requests made to the server crashed. In that case,
+ // if we reuse the same promise, then the switch to the view is
+ // definitely blocked. We want to use a new controller, even though
+ // it is very likely that it will recrash again. At least, it will
+ // give more feedback to the user, and it could happen that one
+ // record crashes, but not another.
+ return newController();
});
+ } else {
+ controllerDef = newController();
}
return this.dp.add(controllerDef).then(function (controller) {
@@ -577,11 +588,11 @@ ActionManager.include({
var env = ev.data.env;
var context = new Context(env.context, actionData.context || {});
var recordID = env.currentID || null; // pyUtils handles null value, not undefined
- var def = $.Deferred();
+ var def;
// determine the action to execute according to the actionData
if (actionData.special) {
- def = $.when({type: 'ir.actions.act_window_close', infos: 'special'});
+ def = Promise.resolve({type: 'ir.actions.act_window_close', infos: 'special'});
} else if (actionData.type === 'object') {
// call a Python Object method, which may return an action to execute
var args = recordID ? [[recordID]] : [env.resIDs];
@@ -611,13 +622,15 @@ ActionManager.include({
active_ids: env.resIDs,
active_id: recordID,
}));
+ } else {
+ def = new Promise(function () {});
}
// use the DropPrevious to prevent from executing the handler if another
// request (doAction, switchView...) has been done meanwhile ; execute
// the fail handler if the 'call_button' or 'loadAction' failed but not
// if the request failed due to the DropPrevious,
- def.fail(ev.data.on_fail);
+ def.guardedCatch(ev.data.on_fail);
this.dp.add(def).then(function (action) {
// show effect if button have effect attribute
// rainbowman can be displayed from two places: from attribute on a button or from python
diff --git a/addons/web/static/src/js/chrome/action_manager_report.js b/addons/web/static/src/js/chrome/action_manager_report.js
index 51922cd719f..30bac14006d 100644
--- a/addons/web/static/src/js/chrome/action_manager_report.js
+++ b/addons/web/static/src/js/chrome/action_manager_report.js
@@ -40,35 +40,36 @@ ActionManager.include({
* report generation and download.
*
* @param {string} url
- * @returns {Deferred} resolved when the report has been downloaded ;
+ * @returns {Promise} resolved when the report has been downloaded ;
* rejected if something went wrong during the report generation
*/
_downloadReport: function (url) {
+ var self = this;
framework.blockUI();
- var def = $.Deferred();
- var type = 'qweb-' + url.split('/')[2];
- var blocked = !session.get_file({
- url: '/report/download',
- data: {
- data: JSON.stringify([url, type]),
- },
- success: def.resolve.bind(def),
- error: function () {
- crash_manager.rpc_error.apply(crash_manager, arguments);
- def.reject();
- },
- complete: framework.unblockUI,
+ return new Promise(function (resolve, reject) {
+ var type = 'qweb-' + url.split('/')[2];
+ var blocked = !session.get_file({
+ url: '/report/download',
+ data: {
+ data: JSON.stringify([url, type]),
+ },
+ success: resolve,
+ error: function () {
+ crash_manager.rpc_error.apply(crash_manager, arguments);
+ reject();
+ },
+ complete: framework.unblockUI,
+ });
+ if (blocked) {
+ // AAB: this check should be done in get_file service directly,
+ // should not be the concern of the caller (and that way, get_file
+ // could return a promise)
+ var message = _t('A popup window with your report was blocked. You ' +
+ 'may need to change your browser settings to allow ' +
+ 'popup windows for this page.');
+ self.do_warn(_t('Warning'), message, true);
+ }
});
- if (blocked) {
- // AAB: this check should be done in get_file service directly,
- // should not be the concern of the caller (and that way, get_file
- // could return a deferred)
- var message = _t('A popup window with your report was blocked. You ' +
- 'may need to change your browser settings to allow ' +
- 'popup windows for this page.');
- this.do_warn(_t('Warning'), message, true);
- }
- return def;
},
/**
@@ -77,7 +78,7 @@ ActionManager.include({
* @private
* @param {Object} action the description of the action to execute
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the action has been executed
+ * @returns {Promise} resolved when the action has been executed
*/
_triggerDownload: function (action, options, type){
var self = this;
@@ -97,7 +98,7 @@ ActionManager.include({
* @private
* @param {Object} action the description of the action to execute
* @param {Object} options @see doAction for details
- * @returns {Deferred} resolved when the action has been executed
+ * @returns {Promise} resolved when the action has been executed
*/
_executeReportAction: function (action, options) {
var self = this;
@@ -125,7 +126,7 @@ ActionManager.include({
} else {
console.error("The ActionManager can't handle reports of type " +
action.report_type, action);
- return $.Deferred().reject();
+ return Promise.reject();
}
},
/**
@@ -135,7 +136,7 @@ ActionManager.include({
*
* @param {Object} action
* @param {Object} options
- * @returns {Deferred} resolved when the client action has been executed
+ * @returns {Promise} resolved when the client action has been executed
*/
_executeReportClientAction: function (action, options) {
var urls = this._makeReportUrls(action);
diff --git a/addons/web/static/src/js/chrome/action_mixin.js b/addons/web/static/src/js/chrome/action_mixin.js
index 92d6716a7c0..02345e7e969 100644
--- a/addons/web/static/src/js/chrome/action_mixin.js
+++ b/addons/web/static/src/js/chrome/action_mixin.js
@@ -75,7 +75,7 @@ var ActionMixin = {
* Called by the action manager when action is restored (typically, when the
* user clicks on the action in the breadcrumb)
*
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
willRestore: function () {},
@@ -89,14 +89,14 @@ var ActionMixin = {
* example, if the user has edited a form, maybe we should ask him if we
* can discard all his changes when we switch to another action. In that
* case, the action manager will call this method. If the returned
- * deferred is succesfully resolved, then we can destroy the current action,
+ * promise is succesfully resolved, then we can destroy the current action,
* otherwise, we need to stop.
*
- * @returns {Deferred} resolved if the action can be removed, rejected
+ * @returns {Promise} resolved if the action can be removed, rejected
* otherwise
*/
canBeRemoved: function () {
- return $.when();
+ return Promise.resolve();
},
/**
* This function is called when the current state of the action
diff --git a/addons/web/static/src/js/chrome/loading.js b/addons/web/static/src/js/chrome/loading.js
index 6c7f102231b..865aa081bf0 100644
--- a/addons/web/static/src/js/chrome/loading.js
+++ b/addons/web/static/src/js/chrome/loading.js
@@ -26,6 +26,8 @@ var Loading = Widget.extend({
this._super(parent);
this.count = 0;
this.blocked_ui = false;
+ },
+ start: function() {
core.bus.on('rpc_request', this, this.request_call);
core.bus.on("rpc_response", this, this.response_call);
core.bus.on("rpc_response_failed", this, this.response_call);
diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js
index 8722724c9a6..552dfce811c 100644
--- a/addons/web/static/src/js/chrome/menu.js
+++ b/addons/web/static/src/js/chrome/menu.js
@@ -64,20 +64,22 @@ var Menu = Widget.extend({
// Apps Menu
this._appsMenu = new AppsMenu(self, this.menu_data);
- this._appsMenu.appendTo(this.$menu_apps);
+ var appsMenuProm = this._appsMenu.appendTo(this.$menu_apps);
// Systray Menu
this.systray_menu = new SystrayMenu(this);
- this.systray_menu.attachTo(this.$('.o_menu_systray'));
-
- dom.initAutoMoreMenu(this.$section_placeholder, {
+ var systrayMenuProm = this.systray_menu.attachTo(this.$('.o_menu_systray')).then(function() {
+ dom.initAutoMoreMenu(self.$section_placeholder, {
maxWidth: function () {
return self.$el.width() - (self.$menu_apps.outerWidth(true) + self.$menu_brand_placeholder.outerWidth(true) + self.systray_menu.$el.outerWidth(true));
},
sizeClass: 'SM',
+ });
});
- return this._super.apply(this, arguments);
+
+
+ return Promise.all([this._super.apply(this, arguments), appsMenuProm, systrayMenuProm]);
},
change_menu_section: function (primary_menu_id) {
if (!this.$menu_sections[primary_menu_id]) {
diff --git a/addons/web/static/src/js/chrome/pager.js b/addons/web/static/src/js/chrome/pager.js
index c7f22a6f2c9..86700d93374 100644
--- a/addons/web/static/src/js/chrome/pager.js
+++ b/addons/web/static/src/js/chrome/pager.js
@@ -28,7 +28,7 @@ var Pager = Widget.extend({
* @param {boolean} [options.can_edit] editable feature of the pager
* @param {boolean} [options.single_page_hidden] (not) to display the pager
* if only one page
- * @param {function} [options.validate] callback returning a Deferred to
+ * @param {function} [options.validate] callback returning a Promise to
* validate changes
*/
init: function (parent, size, current_min, limit, options) {
@@ -46,7 +46,7 @@ var Pager = Widget.extend({
can_edit: true, // editable
single_page_hidden: false, // displayed even if there is a single page
validate: function() {
- return $.Deferred().resolve();
+ return Promise.resolve();
},
withAccessKey: true, // can be disabled, for example, for x2m widgets
});
@@ -55,7 +55,7 @@ var Pager = Widget.extend({
/**
* Renders the pager
*
- * @returns {jQuery.Deferred}
+ * @returns {Promise}
*/
start: function () {
this.$value = this.$('.o_pager_value');
@@ -216,7 +216,6 @@ var Pager = Widget.extend({
}
self.trigger('pager_changed', _.clone(self.state));
}
- }).always(function() {
// Render the pager's new state (removes the input)
self._render();
});
diff --git a/addons/web/static/src/js/chrome/sidebar.js b/addons/web/static/src/js/chrome/sidebar.js
index 5cf6005a788..b7883cbb0f7 100644
--- a/addons/web/static/src/js/chrome/sidebar.js
+++ b/addons/web/static/src/js/chrome/sidebar.js
@@ -153,7 +153,7 @@ var Sidebar = Widget.extend({
action_id: item.action.id,
context: context,
},
- }).done(function (result) {
+ }).then(function (result) {
result.context = new Context(
result.context || {}, activeIdsContext)
.set_eval_context(context);
diff --git a/addons/web/static/src/js/chrome/systray_menu.js b/addons/web/static/src/js/chrome/systray_menu.js
index 24fd02a3736..fd214c0b650 100644
--- a/addons/web/static/src/js/chrome/systray_menu.js
+++ b/addons/web/static/src/js/chrome/systray_menu.js
@@ -16,20 +16,19 @@ var SystrayMenu = Widget.extend({
this._super(parent);
this.items = [];
this.widgets = [];
- this.load = $.Deferred();
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
var self = this;
self._super.apply(this, arguments);
self._loadItems();
- $.when.apply($, self.items).always(function () {
- self.load.resolve();
+
+ return new Promise(function (resolve, reject) {
+ Promise.all(self.items).then(resolve).guardedCatch(resolve);
});
- return self.load;
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/chrome/user_menu.js b/addons/web/static/src/js/chrome/user_menu.js
index a066b661fb2..2cc39b4d6bd 100644
--- a/addons/web/static/src/js/chrome/user_menu.js
+++ b/addons/web/static/src/js/chrome/user_menu.js
@@ -22,7 +22,7 @@ var UserMenu = Widget.extend({
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
var self = this;
@@ -36,7 +36,7 @@ var UserMenu = Widget.extend({
var $avatar = self.$('.oe_topbar_avatar');
if (!session.uid) {
$avatar.attr('src', $avatar.data('default-src'));
- return $.when();
+ return Promise.resolve();
}
var topbar_name = session.name;
if (session.debug) {
@@ -67,7 +67,7 @@ var UserMenu = Widget.extend({
.then(function (url) {
framework.redirect(url);
})
- .fail(function (result, ev){
+ .guardedCatch(function (result, ev){
ev.preventDefault();
framework.redirect('https://accounts.odoo.com/account');
});
@@ -100,7 +100,7 @@ var UserMenu = Widget.extend({
model: "res.users",
method: "action_get"
})
- .done(function (result) {
+ .then(function (result) {
result.res_id = session.uid;
self.do_action(result);
});
diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js
index 0688adc362f..392d534f372 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -111,7 +111,7 @@ return AbstractWebClient.extend({
instanciate_menu_widgets: function () {
var self = this;
- var defs = [];
+ var proms = [];
return this.load_menus().then(function (menuData) {
self.menu_data = menuData;
@@ -122,8 +122,8 @@ return AbstractWebClient.extend({
self.menu.destroy();
}
self.menu = new Menu(self, menuData);
- defs.push(self.menu.prependTo(self.$el));
- return $.when.apply($, defs);
+ proms.push(self.menu.prependTo(self.$el));
+ return Promise.all(proms);
});
},
@@ -133,7 +133,7 @@ return AbstractWebClient.extend({
on_hashchange: function (event) {
if (this._ignore_hashchange) {
this._ignore_hashchange = false;
- return $.when();
+ return Promise.resolve();
}
var self = this;
@@ -181,48 +181,51 @@ return AbstractWebClient.extend({
return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
.then(function (result) {
return self.action_mutex.exec(function () {
- var completed = $.Deferred();
- var options = _.extend({}, ev.data.options, {
- clear_breadcrumbs: true,
- action_menu_id: ev.data.menu_id,
- });
- $.when(self._openMenu(result, options)).fail(function () {
- completed.resolve();
- }).done(function () {
- self._on_app_clicked_done(ev)
- .then(completed.resolve.bind(completed))
- .fail(completed.reject.bind(completed));
+ var completed = new Promise(function (resolve, reject) {
+ var options = _.extend({}, ev.data.options, {
+ clear_breadcrumbs: true,
+ action_menu_id: ev.data.menu_id,
+ });
+
+ Promise.resolve(self._openMenu(result, options))
+ .then(function() {
+ self._on_app_clicked_done(ev)
+ .then(resolve)
+ .guardedCatch(reject);
+ }).guardedCatch(function() {
+ resolve();
+ });
+ setTimeout(function () {
+ resolve();
+ }, 2000);
});
- setTimeout(function () {
- completed.resolve();
- }, 2000);
return completed;
});
});
},
_on_app_clicked_done: function (ev) {
core.bus.trigger('change_menu_section', ev.data.menu_id);
- return $.Deferred().resolve();
+ return Promise.resolve();
},
on_menu_clicked: function (ev) {
var self = this;
return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
.then(function (result) {
- return self.action_mutex.exec(function () {
- var completed = $.Deferred();
- $.when(self._openMenu(result, {
- clear_breadcrumbs: true,
- })).always(function () {
- completed.resolve();
- });
+ self.$el.removeClass('o_mobile_menu_opened');
- setTimeout(function () {
- completed.resolve();
- }, 2000);
+ return self.action_mutex.exec(function () {
+ var completed = new Promise(function (resolve, reject) {
+ Promise.resolve(self._openMenu(result, {
+ clear_breadcrumbs: true,
+ })).then(resolve).guardedCatch(reject);
+ setTimeout(function () {
+ resolve();
+ }, 2000);
+ });
return completed;
});
- }).always(function () {
+ }).guardedCatch(function () {
self.$el.removeClass('o_mobile_menu_opened');
});
},
@@ -233,7 +236,7 @@ return AbstractWebClient.extend({
* @private
* @param {Object} action
* @param {Object} options
- * @returns {Deferred}
+ * @returns {Promise}
*/
_openMenu: function (action, options) {
return this.do_action(action, options);
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;
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index edc7c03ac02..822c90c31f4 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -184,7 +184,7 @@ var AbstractField = Widget.extend({
* When a field widget is appended to the DOM, its start method is called,
* and will automatically call render. Most widgets should not override this.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
var self = this;
@@ -236,7 +236,7 @@ var AbstractField = Widget.extend({
* if the value changed but was not notified.
*
* @abstract
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
commitChanges: function () {},
/**
@@ -290,12 +290,12 @@ var AbstractField = Widget.extend({
* is optional, and may be used by a widget to share information from the
* moment a field change event is triggered to the moment a reset
* operation is applied.
- * @returns {Deferred} A Deferred, which resolves when the widget rendering
+ * @returns {Promise} A promise, which resolves when the widget rendering
* is complete
*/
reset: function (record, event) {
this._reset(record, event);
- return this._render() || $.when();
+ return this._render() || Promise.resolve();
},
/**
* Remove the invalid class on a field
@@ -396,7 +396,7 @@ var AbstractField = Widget.extend({
* synchronous.
*
* @private
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
_render: function () {
if (this.attrs.decorations) {
@@ -413,7 +413,7 @@ var AbstractField = Widget.extend({
* concrete widget.
*
* @private
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
_renderEdit: function () {
},
@@ -422,7 +422,7 @@ var AbstractField = Widget.extend({
* the concrete widget.
*
* @private
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
_renderReadonly: function () {
},
@@ -457,13 +457,13 @@ var AbstractField = Widget.extend({
* will not notify and not trigger the onchange, even though it was changed.
* @param {boolean} [options.forceChange=false] if true, the change event will be
* triggered even if the new value is the same as the old one
- * @returns {Deferred}
+ * @returns {Promise}
*/
_setValue: function (value, options) {
// we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values.
if (this.lastSetValue === value || (this.value === false && value === '')) {
- return $.when();
+ return Promise.resolve();
}
this.lastSetValue = value;
try {
@@ -472,25 +472,26 @@ var AbstractField = Widget.extend({
} catch (e) {
this._isValid = false;
this.trigger_up('set_dirty', {dataPointID: this.dataPointID});
- return $.Deferred().reject();
+ return Promise.reject({message: "Value set is not valid"});
}
if (!(options && options.forceChange) && this._isSameValue(value)) {
- return $.when();
+ return Promise.resolve();
}
- var def = $.Deferred();
- var changes = {};
- changes[this.name] = value;
- this.trigger_up('field_changed', {
- dataPointID: this.dataPointID,
- changes: changes,
- viewType: this.viewType,
- doNotSetDirty: options && options.doNotSetDirty,
- notifyChange: !options || options.notifyChange !== false,
- allowWarning: options && options.allowWarning,
- onSuccess: def.resolve.bind(def),
- onFailure: def.reject.bind(def),
+ var self = this;
+ return new Promise(function (resolve, reject) {
+ var changes = {};
+ changes[self.name] = value;
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: changes,
+ viewType: self.viewType,
+ doNotSetDirty: options && options.doNotSetDirty,
+ notifyChange: !options || options.notifyChange !== false,
+ allowWarning: options && options.allowWarning,
+ onSuccess: resolve,
+ onFailure: reject,
+ });
});
- return def;
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index c6df3885a86..a87ff3e4436 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -213,7 +213,7 @@ var InputField = DebouncedField.extend({
// sure they are recomputed.
this._applyDecorations();
}
- return $.when();
+ return Promise.resolve();
} else {
return this._render();
}
@@ -486,8 +486,6 @@ var LinkButton = AbstractField.extend({
});
-
-
var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
@@ -513,7 +511,7 @@ var FieldDate = InputField.extend({
*/
start: function () {
var self = this;
- var def;
+ var prom;
if (this.mode === 'edit') {
this.datewidget = this._makeDatePicker();
this.datewidget.on('datetime_changed', this, function () {
@@ -522,13 +520,13 @@ var FieldDate = InputField.extend({
this._setValue(value);
}
});
- def = this.datewidget.appendTo('<div>').done(function () {
+ prom = this.datewidget.appendTo('<div>').then(function () {
self.datewidget.$el.addClass(self.$el.attr('class'));
self._prepareInput(self.datewidget.$input);
self._replaceElement(self.datewidget.$el);
});
}
- return $.when(def, this._super.apply(this, arguments));
+ return Promise.resolve(prom).then(this._super.bind(this));
},
//--------------------------------------------------------------------------
@@ -728,7 +726,7 @@ var FieldMonetary = InputField.extend({
this.$el.empty();
// Prepare and add the input
- this._prepareInput(this.$input).appendTo(this.$el);
+ var def = this._prepareInput(this.$input).appendTo(this.$el);
if (this.currency) {
// Prepare and add the currency symbol
@@ -739,6 +737,7 @@ var FieldMonetary = InputField.extend({
this.$el.prepend($currencySymbol);
}
}
+ return def;
},
/**
* @override
@@ -1136,7 +1135,7 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
*/
reset: function () {
var self = this;
- return $.when(this._super.apply(this, arguments)).then(function () {
+ return Promise.resolve(this._super.apply(this, arguments)).then(function () {
if (self.mode === 'edit') {
self.$input.trigger('change');
}
@@ -2568,7 +2567,7 @@ var JournalDashboardGraph = AbstractField.extend({
if (this._isInDOM) {
return this._renderInDOM();
}
- return $.when();
+ return Promise.resolve();
},
/**
* Render the widget. This function assumes that it is attached to the DOM.
@@ -2743,13 +2742,13 @@ var FieldDomain = AbstractField.extend({
/**
* @private
* @override _render from AbstractField
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
// If there is no model, only change the non-domain-selector content
if (!this._domainModel) {
this._replaceContent();
- return $.when();
+ return Promise.resolve();
}
// Convert char value to array value
@@ -2901,7 +2900,7 @@ var AceEditor = DebouncedField.extend({
/**
* @override start from AbstractField (Widget)
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this._startAce(this.$('.ace-view-editor')[0]);
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 35d10c5a5e4..0e0ba3dc117 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -6,7 +6,7 @@ var Registry = require('web.Registry');
return new Registry();
});
-odoo.define('web._field_registry', function(require) {
+odoo.define('web._field_registry', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index bd46faf1e08..469cedb8ff1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -171,11 +171,11 @@ var FieldMany2One = AbstractField.extend({
* is saved.
*
* @override
- * @returns {Deferred} resolved as soon as there is no longer record being
+ * @returns {Promise} resolved as soon as there is no longer record being
* (quick) created
*/
commitChanges: function () {
- return $.when(this.createDef);
+ return Promise.resolve(this.createDef);
},
/**
* @override
@@ -208,7 +208,7 @@ var FieldMany2One = AbstractField.extend({
this.isDirty = false;
}
if (this.isDirty) {
- return $.when();
+ return Promise.resolve();
} else {
return this._render();
}
@@ -257,9 +257,9 @@ var FieldMany2One = AbstractField.extend({
if (!source.validation || source.validation.call(self, req.term)) {
source.loading = true;
- // Wrap the returned value of the source.method with $.when.
+ // Wrap the returned value of the source.method with a promise
// So event if the returned value is not async, it will work
- $.when(source.method.call(self, req.term)).then(function (results) {
+ Promise.resolve(source.method.call(self, req.term)).then(function (results) {
source.results = results;
source.loading = false;
resp(self._concatenateAutocompleteResults());
@@ -366,49 +366,55 @@ var FieldMany2One = AbstractField.extend({
/**
* @private
* @param {string} name
- * @returns {Deferred} resolved after the name_create or when the slowcreate
+ * @returns {Promise} resolved after the name_create or when the slowcreate
* modal is closed.
*/
_quickCreate: function (name) {
var self = this;
- var def = $.Deferred();
- this.createDef = this.createDef || $.Deferred();
- // called when the record has been quick created, or when the dialog has
- // been closed (in the case of a 'slow' create), meaning that the job is
- // done
- var createDone = function () {
- def.resolve();
- self.createDef.resolve();
- self.createDef = undefined;
- };
- // called if the quick create is disabled on this many2one, or if the
- // quick creation failed (probably because there are mandatory fields on
- // the model)
- var slowCreate = function () {
- var dialog = self._searchCreatePopup("form", false, self._createContext(name));
- dialog.on('closed', self, createDone);
- };
- if (this.nodeOptions.quick_create) {
- var nameCreateDef = this._rpc({
- model: this.field.relation,
- method: 'name_create',
- args: [name],
- context: this.record.getContext(this.recordParams),
- }).fail(function (error, ev) {
- ev.preventDefault();
- slowCreate();
+ var createDone;
+
+ var def = new Promise(function (resolve, reject) {
+ self.createDef = new Promise(function (innerResolve) {
+ // called when the record has been quick created, or when the dialog has
+ // been closed (in the case of a 'slow' create), meaning that the job is
+ // done
+ createDone = function () {
+ innerResolve();
+ resolve();
+ self.createDef = undefined;
+ };
});
- this.dp.add(nameCreateDef)
- .then(function (result) {
- if (self.mode === "edit") {
- self.reinitialize({id: result[0], display_name: result[1]});
- }
- createDone();
- })
- .fail(def.reject.bind(def));
- } else {
- slowCreate();
- }
+
+ // called if the quick create is disabled on this many2one, or if the
+ // quick creation failed (probably because there are mandatory fields on
+ // the model)
+ var slowCreate = function () {
+ var dialog = self._searchCreatePopup("form", false, self._createContext(name));
+ dialog.on('closed', self, createDone);
+ };
+ if (self.nodeOptions.quick_create) {
+ var nameCreateDef = self._rpc({
+ model: self.field.relation,
+ method: 'name_create',
+ args: [name],
+ context: self.record.getContext(self.recordParams),
+ }).guardedCatch(function (reason) {
+ reason.event.preventDefault();
+ slowCreate();
+ });
+ self.dp.add(nameCreateDef)
+ .then(function (result) {
+ if (self.mode === "edit") {
+ self.reinitialize({id: result[0], display_name: result[1]});
+ }
+ createDone();
+ })
+ .guardedCatch(reject);
+ } else {
+ slowCreate();
+ }
+ });
+
return def;
},
/**
@@ -456,35 +462,32 @@ var FieldMany2One = AbstractField.extend({
*
* @private
* @param {string} search_val
- * @returns {Deferred}
+ * @returns {Promise}
*/
_search: function (search_val) {
var self = this;
- var def = $.Deferred();
- this.orderer.add(def);
-
- var context = this.record.getContext(this.recordParams);
- var domain = this.record.getDomain(this.recordParams);
+ var def = new Promise(function (resolve, reject) {
+ var context = self.record.getContext(self.recordParams);
+ var domain = self.record.getDomain(self.recordParams);
- // Add the additionalContext
- _.extend(context, this.additionalContext);
+ // Add the additionalContext
+ _.extend(context, self.additionalContext);
- var blacklisted_ids = this._getSearchBlacklist();
- if (blacklisted_ids.length > 0) {
- domain.push(['id', 'not in', blacklisted_ids]);
- }
+ var blacklisted_ids = self._getSearchBlacklist();
+ if (blacklisted_ids.length > 0) {
+ domain.push(['id', 'not in', blacklisted_ids]);
+ }
- this._rpc({
- model: this.field.relation,
- method: "name_search",
- kwargs: {
- name: search_val,
- args: domain,
- operator: "ilike",
- limit: this.limit + 1,
- context: context,
- }})
- .then(function (result) {
+ self._rpc({
+ model: self.field.relation,
+ method: "name_search",
+ kwargs: {
+ name: search_val,
+ args: domain,
+ operator: "ilike",
+ limit: self.limit + 1,
+ context: context,
+ }}).then(function (result) {
// possible selections for the m2o
var values = _.map(result, function (x) {
x[1] = self._getDisplayName(x[1]);
@@ -516,7 +519,7 @@ var FieldMany2One = AbstractField.extend({
},
});
}
- $.when(prom).then(function (results) {
+ Promise.resolve(prom).then(function (results) {
var dynamicFilters;
if (results) {
var ids = _.map(results, function (x) {
@@ -563,9 +566,10 @@ var FieldMany2One = AbstractField.extend({
});
}
- def.resolve(values);
+ resolve(values);
});
-
+ });
+ this.orderer.add(def);
return def;
},
/**
@@ -670,8 +674,9 @@ var FieldMany2One = AbstractField.extend({
readonly: !self.can_write,
on_saved: function (record, changed) {
if (changed) {
- self._setValue(self.value.data, {forceChange: true});
- self.trigger_up('reload', {db_id: self.value.id});
+ self._setValue(self.value.data, {forceChange: true}).then(function() {
+ self.trigger_up('reload', {db_id: self.value.id});
+ });
}
},
}).open();
@@ -868,14 +873,14 @@ var FieldX2Many = AbstractField.extend({
* user if he wants to discard it if necessary.
*
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
commitChanges: function () {
var self = this;
var inEditionRecordID =
- this.renderer
- && this.renderer.viewType === "list"
- && this.renderer.getEditableRecordID();
+ this.renderer &&
+ this.renderer.viewType === "list" &&
+ this.renderer.getEditableRecordID();
if (inEditionRecordID) {
return this.renderer.commitChanges(inEditionRecordID).then(function () {
return self._saveLine(inEditionRecordID);
@@ -894,7 +899,7 @@ var FieldX2Many = AbstractField.extend({
* @param {Object} record
* @param {OdooEvent} [ev] an event that triggered the reset action
* @param {Boolean} [fieldChanged] if true, the widget field has changed
- * @returns {Deferred}
+ * @returns {Promise}
*/
reset: function (record, ev, fieldChanged) {
// If 'fieldChanged' is false, it means that the reset was triggered by
@@ -904,7 +909,7 @@ var FieldX2Many = AbstractField.extend({
if (!fieldChanged) {
var newEval = this._evalColumnInvisibleFields();
if (_.isEqual(this.currentColInvisibleFields, newEval)) {
- return $.when();
+ return Promise.resolve();
}
} else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name];
@@ -994,17 +999,20 @@ var FieldX2Many = AbstractField.extend({
*
* @override
* @private
- * @returns {Deferred|undefined}
+ * @returns {Promise|undefined}
*/
_render: function () {
+ var self = this;
if (!this.view) {
return this._super();
}
if (this.renderer) {
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
- this.renderer.updateState(this.value, {'columnInvisibleFields': this.currentColInvisibleFields});
- this.pager.updateState({ size: this.value.count });
- return $.when();
+ return this.renderer.updateState(this.value, {
+ columnInvisibleFields: this.currentColInvisibleFields,
+ }).then(function () {
+ self.pager.updateState({ size: self.value.count });
+ });
}
var arch = this.view.arch;
var viewType;
@@ -1050,11 +1058,11 @@ var FieldX2Many = AbstractField.extend({
* Prepends the control panel's $el to this widget's $el.
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderControlPanel: function () {
if (!this.view) {
- return $.when();
+ return Promise.resolve();
}
var self = this;
var defs = [];
@@ -1072,7 +1080,7 @@ var FieldX2Many = AbstractField.extend({
validate: function () {
var isList = self.view.arch.tag === 'tree';
// TODO: we should have some common method in the basic renderer...
- return isList ? self.renderer.unselectRow() : $.when();
+ return isList ? self.renderer.unselectRow() : Promise.resolve();
},
});
this.pager.on('pager_changed', this, function (new_state) {
@@ -1089,7 +1097,7 @@ var FieldX2Many = AbstractField.extend({
this._renderButtons();
defs.push(this.pager.appendTo($('<div>'))); // start the pager
defs.push(cpDef);
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
self._controlPanel.updateContents({
cp_content: {
$buttons: self.$buttons,
@@ -1123,24 +1131,24 @@ var FieldX2Many = AbstractField.extend({
*
* @private
* @param {string} recordID
- * @returns {Deferred} resolved if the line was properly saved or discarded.
+ * @returns {Promise} 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();
- var fieldNames = this.renderer.canBeSaved(recordID);
- if (fieldNames.length) {
- this.trigger_up('discard_changes', {
- recordID: recordID,
- onSuccess: def.resolve.bind(def),
- onFailure: def.reject.bind(def),
- });
- } else {
- this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def));
- }
- return def.then(function () {
+ return new Promise(function (resolve, reject) {
+ var fieldNames = self.renderer.canBeSaved(recordID);
+ if (fieldNames.length) {
+ self.trigger_up('discard_changes', {
+ recordID: recordID,
+ onSuccess: resolve,
+ onFailure: reject,
+ });
+ } else {
+ self.renderer.setRowMode(recordID, 'readonly').then(resolve);
+ }
+ }).then(function () {
self.pager.updateState({ size: self.value.count });
var newEval = self._evalColumnInvisibleFields();
if (!_.isEqual(self.currentColInvisibleFields, newEval)) {
@@ -1233,7 +1241,7 @@ var FieldX2Many = AbstractField.extend({
this.trigger_up('edited_list', { id: this.value.id });
var editedRecord = this.value.data[ev.data.index];
this.renderer.setRowMode(editedRecord.id, 'edit')
- .done(ev.data.onSuccess);
+ .then(ev.data.onSuccess);
},
/**
* Updates the given record with the changes.
@@ -1260,11 +1268,11 @@ var FieldX2Many = AbstractField.extend({
operation: 'UPDATE',
id: ev.data.dataPointID,
data: changes,
- }).done(function () {
+ }).then(function () {
if (ev.data.onSuccess) {
ev.data.onSuccess();
}
- }).fail(function () {
+ }).guardedCatch(function () {
if (ev.data.onFailure) {
ev.data.onFailure();
}
@@ -1332,8 +1340,8 @@ var FieldX2Many = AbstractField.extend({
self.trigger_up('mutexify', {
action: function () {
return self._saveLine(ev.data.recordID)
- .done(ev.data.onSuccess)
- .fail(ev.data.onFailure);
+ .then(ev.data.onSuccess)
+ .guardedCatch(ev.data.onFailure);
},
});
});
@@ -1361,18 +1369,20 @@ var FieldX2Many = AbstractField.extend({
notifyChange: false,
});
});
- $.when.apply($, defs).then(function () {
+ Promise.all(defs).then(function () {
+ function always() {
+ self.trigger_up('toggle_column_order', {
+ id: self.value.id,
+ name: event.data.handleField,
+ });
+ }
+
// trigger only once the onchange for parent record
self._setValue({
operation: 'UPDATE',
id: rowID,
data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]),
- }).always(function () {
- self.trigger_up('toggle_column_order', {
- id: self.value.id,
- name: event.data.handleField,
- });
- });
+ }).then(always).guardedCatch(always);
});
},
/**
@@ -1418,7 +1428,7 @@ var FieldOne2Many = FieldX2Many.extend({
* @override
* @param {Object} record
* @param {OdooEvent} [ev] an event that triggered the reset action
- * @returns {Deferred}
+ * @returns {Promise}
*/
reset: function (record, ev) {
var self = this;
@@ -1527,13 +1537,16 @@ var FieldOne2Many = FieldX2Many.extend({
context: data.context,
}, {
allowWarning: data.allowWarning
- }).always(function () {
+ }).then(function () {
self.creatingRecord = false;
- }).done(function (){
+ }).then(function (){
if (data.onSuccess){
data.onSuccess();
}
- });
+ }).guardedCatch(function() {
+ self.creatingRecord = false;
+ })
+ ;
}
} else {
this._openFormDialog({
@@ -1646,8 +1659,10 @@ var FieldMany2Many = FieldX2Many.extend({
domain: this.record.getDomain(this.recordParams),
fields_view: this.attrs.views && this.attrs.views.form,
on_saved: function () {
- self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true});
- self.trigger_up('reload', {db_id: ev.data.id});
+ self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true})
+ .then(function () {
+ self.trigger_up('reload', {db_id: ev.data.id});
+ });
},
on_remove: function () {
self._setValue({operation: 'FORGET', ids: [ev.data.id]});
@@ -1918,10 +1933,12 @@ var FieldMany2ManyTags = AbstractField.extend({
* @override
*/
reset: function (record, event) {
- this._super.apply(this, arguments);
- if (event && event.target === this) {
- this.activate();
- }
+ var self = this;
+ return this._super.apply(this, arguments).then(function(){
+ if (event && event.target === self) {
+ self.activate();
+ }
+ });
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/upgrade_fields.js b/addons/web/static/src/js/fields/upgrade_fields.js
index dd91ca2efc2..8dd1f9d3ae8 100644
--- a/addons/web/static/src/js/fields/upgrade_fields.js
+++ b/addons/web/static/src/js/fields/upgrade_fields.js
@@ -34,7 +34,7 @@ var AbstractFieldUpgrade = {
* Redirects the user to the odoo-enterprise/uprade page
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmUpgrade: function () {
return this._rpc({
diff --git a/addons/web/static/src/js/promise_extension.js b/addons/web/static/src/js/promise_extension.js
new file mode 100644
index 00000000000..4758975c712
--- /dev/null
+++ b/addons/web/static/src/js/promise_extension.js
@@ -0,0 +1,22 @@
+/**
+ * This file adds a 'guardedCatch' function to the Promise API. This function
+ * has to be used when we don't want to swallow real errors (crashes), like
+ * 'catch' does (i.e. basically all the time in Odoo). We only execute the
+ * 'onRejected' handler if the rejection's reason is not an Error, and we always
+ * return a rejected Promise to let the rejection bubble up (and trigger the
+ * 'unhandledrejection' event).
+ */
+
+(function () {
+ var _catch = Promise.prototype.catch;
+ Promise.prototype.guardedCatch = function (onRejected) {
+ return _catch.call(this, function (reason) {
+ if (!reason || !(reason instanceof Error)) {
+ if (onRejected) {
+ onRejected.call(this, reason);
+ }
+ }
+ return Promise.reject(reason);
+ });
+ };
+})();
diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js
index 7c508bd9192..97448408282 100644
--- a/addons/web/static/src/js/public/public_root.js
+++ b/addons/web/static/src/js/public/public_root.js
@@ -49,11 +49,11 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
*/
willStart: function () {
// TODO would be even greater to wait for localeDef only when necessary
- return $.when(
+ return Promise.all([
this._super.apply(this, arguments),
session.is_bound,
localeDef
- );
+ ]);
},
/**
* @override
@@ -82,7 +82,7 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
$('input, textarea').placeholder();
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
//--------------------------------------------------------------------------
@@ -193,9 +193,9 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
self.publicWidgets.push(widget);
return widget.attachTo($(el));
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Destroys all registered widget instances. Website would need this before
@@ -259,8 +259,8 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
*/
_onWidgetsStartRequest: function (ev) {
this._startWidgets(ev.data.$target, ev.data.options)
- .done(ev.data.onSuccess)
- .fail(ev.data.onFailure);
+ .then(ev.data.onSuccess)
+ .guardedCatch(ev.data.onFailure);
},
/**
* Called when the root is notified that the public widgets have to be
diff --git a/addons/web/static/src/js/public/public_widget.js b/addons/web/static/src/js/public/public_widget.js
index ec9c50ad9a8..79382e117d8 100644
--- a/addons/web/static/src/js/public/public_widget.js
+++ b/addons/web/static/src/js/public/public_widget.js
@@ -43,7 +43,7 @@ var RootWidget = Widget.extend({
defs.push(this._attachComponents());
this._listenToUpdates = true;
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
//--------------------------------------------------------------------------
@@ -71,7 +71,7 @@ var RootWidget = Widget.extend({
self._widgets.push(w);
return w.attachTo(element);
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Instantiates the child widgets that need to be according to the linked
@@ -88,7 +88,7 @@ var RootWidget = Widget.extend({
var defs = _.map(childInfos, function (childInfo) {
return self._attachComponent(childInfo, $from);
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Returns the `RootWidgetRegistry` instance that is linked to this
diff --git a/addons/web/static/src/js/report/client_action.js b/addons/web/static/src/js/report/client_action.js
index 099ee77a527..c8b5621e6de 100644
--- a/addons/web/static/src/js/report/client_action.js
+++ b/addons/web/static/src/js/report/client_action.js
@@ -37,7 +37,7 @@ var ReportAction = AbstractAction.extend({
start: function () {
var self = this;
this.iframe = this.$('iframe')[0];
- return $.when(this._super.apply(this, arguments), session.is_bound).then(function () {
+ return Promise.all([this._super.apply(this, arguments), session.is_bound]).then(function () {
var web_base_url = session['web.base.url'];
var trusted_host = utils.get_host_from_url(web_base_url);
var trusted_protocol = utils.get_protocol_from_url(web_base_url);
@@ -105,7 +105,7 @@ var ReportAction = AbstractAction.extend({
/**
* Helper allowing to send a message to the `this.el` iframe's window and
- * seting the `targetOrigin` as `this.trusted_origin` (which is the
+ * seting the `targetOrigin` as `this.trusted_origin` (which is the
* `web.base.url` ir.config_parameter key) - in other word, only when using
* this method we only send the message to a trusted domain.
*/
diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js
index 0cecaae9da3..cb83dd9d553 100644
--- a/addons/web/static/src/js/services/ajax_service.js
+++ b/addons/web/static/src/js/services/ajax_service.js
@@ -7,19 +7,20 @@ var session = require('web.session');
var AjaxService = AbstractService.extend({
rpc: function (route, args, options, target) {
- var def = $.Deferred();
- var promise = def.promise();
- var xhrDef = session.rpc(route, args, options);
- promise.abort = xhrDef.abort.bind(xhrDef);
- xhrDef.then(function () {
- if (!target.isDestroyed()) {
- def.resolve.apply(def, arguments);
- }
- }).fail(function () {
- if (!target.isDestroyed()) {
- def.reject.apply(def, arguments);
- }
+ var rpcPromise;
+ var promise = new Promise(function (resolve, reject) {
+ rpcPromise = session.rpc(route, args, options);
+ rpcPromise.then(function (result) {
+ if (!target.isDestroyed()) {
+ resolve(result);
+ }
+ }).guardedCatch(function (reason) {
+ if (!target.isDestroyed()) {
+ reject(reason);
+ }
+ });
});
+ promise.abort = rpcPromise.abort.bind(rpcPromise);
return promise;
},
});
diff --git a/addons/web/static/src/js/services/core.js b/addons/web/static/src/js/services/core.js
index 3d6cfc4e684..ee37479e217 100644
--- a/addons/web/static/src/js/services/core.js
+++ b/addons/web/static/src/js/services/core.js
@@ -13,15 +13,15 @@ var translation = require('web.translation');
*
* @type Boolean
*/
-var bus = new Bus ();
+var bus = new Bus();
-_.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
- $('html').on(evtype, function(ev) {
+_.each('click,dblclick,keydown,keypress,keyup'.split(','), function (evtype) {
+ $('html').on(evtype, function (ev) {
bus.trigger(evtype, ev);
});
});
-_.each('resize,scroll'.split(','), function(evtype) {
- $(window).on(evtype, function(ev) {
+_.each('resize,scroll'.split(','), function (evtype) {
+ $(window).on(evtype, function (ev) {
bus.trigger(evtype, ev);
});
});
@@ -37,7 +37,7 @@ return {
_lt: translation._lt,
// registries
- action_registry : new Registry(),
+ action_registry: new Registry(),
crash_registry: new Registry(),
serviceRegistry: new Registry(),
/**
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index 31aa178dc1b..a29e21ca8ca 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -20,9 +20,29 @@ var map_title ={
};
var CrashManager = core.Class.extend({
- init: function() {
+ init: function () {
+ var self = this;
this.active = true;
this.isConnected = true;
+
+ // listen to unhandled rejected promises, and throw an error when the
+ // promise has been rejected due to a crash
+ window.addEventListener('unhandledrejection', function (ev) {
+ if (ev.reason && ev.reason instanceof Error) {
+ var traceback = ev.reason.stack;
+ self.show_error({
+ type: _t("Odoo Client Error"),
+ message: '',
+ data: {debug: _t('Traceback:') + "\n" + traceback},
+ });
+ } else {
+ // the rejection is not due to an Error, so prevent the browser
+ // from displaying an 'unhandledrejection' error in the console
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ ev.preventDefault();
+ }
+ });
},
enable: function () {
this.active = true;
@@ -45,7 +65,7 @@ var CrashManager = core.Class.extend({
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
core.bus.trigger('connection_restored');
self.isConnected = true;
- }).fail(function () {
+ }).guardedCatch(function () {
// exponential backoff, with some jitter
delay = (delay * 1.5) + 500*Math.random();
setTimeout(checkConnection, delay);
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 348054691bc..b2f6ef1feb1 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -33,7 +33,7 @@ return core.Class.extend({
*
* @param {int|string} [action_id] the action id or xmlid
* @param {Object} [additional_context] used to load the action
- * @return {Deferred} resolved with the action whose id or xmlid is action_id
+ * @return {Promise} resolved with the action whose id or xmlid is action_id
*/
load_action: function (action_id, additional_context) {
var self = this;
@@ -69,7 +69,7 @@ return core.Class.extend({
* - options.load_filters: whether or not to load the filters,
* - options.action_id: the action_id (required to load filters),
* - options.toolbar: whether or not a toolbar will be displayed,
- * @return {Deferred} resolved with the requested views information
+ * @return {Promise} resolved with the requested views information
*/
load_views: function (params, options) {
var self = this;
@@ -108,12 +108,12 @@ return core.Class.extend({
var fvg = result.fields_views[view_descr[1]];
fvg.viewFields = fvg.fields;
fvg.fields = result.fields;
- self._cache.fields_views[fv_key] = $.when(fvg);
+ self._cache.fields_views[fv_key] = Promise.resolve(fvg);
});
// Insert filters, if any, into the filters cache
if (result.filters) {
- self._cache.filters[filters_key] = $.when(result.filters);
+ self._cache.filters[filters_key] = Promise.resolve(result.filters);
}
return result.fields_views;
@@ -130,7 +130,7 @@ return core.Class.extend({
* @param {string} params.modelName
* @param {Object} params.context
* @param {integer} params.actionId
- * @return {Deferred} resolved with the requested filters
+ * @return {Promise} resolved with the requested filters
*/
load_filters: function (params) {
var key = this._gen_key(params.modelName, params.actionId);
@@ -143,7 +143,7 @@ return core.Class.extend({
},
model: 'ir.filters',
method: 'get_filters',
- }).fail(this._invalidate.bind(this, this._cache.filters, key));
+ }).guardedCatch(this._invalidate.bind(this, this._cache.filters, key));
}
return this._cache.filters[key];
},
@@ -152,7 +152,7 @@ return core.Class.extend({
* Calls 'create_or_replace' on 'ir_filters'.
*
* @param {Object} [filter] the filter description
- * @return {Deferred} resolved with the id of the created or replaced filter
+ * @return {Promise} resolved with the id of the created or replaced filter
*/
create_filter: function (filter) {
var self = this;
@@ -175,7 +175,7 @@ return core.Class.extend({
* Calls 'unlink' on 'ir_filters'.
*
* @param {integer} filterId Id of the filter to remove
- * @return {Deferred}
+ * @return {Promise}
*/
delete_filter: function (filterId) {
var self = this;
diff --git a/addons/web/static/src/js/services/report_service.js b/addons/web/static/src/js/services/report_service.js
index 006c546041a..ed4907a6d6d 100644
--- a/addons/web/static/src/js/services/report_service.js
+++ b/addons/web/static/src/js/services/report_service.js
@@ -15,7 +15,7 @@ var ReportService = AbstractService.extend({
* Checks the state of the installation of wkhtmltopdf on the server.
* Implements an internal cache to do the request only once.
*
- * @returns {Deferred} resolved with the state of wkhtmltopdf on the server
+ * @returns {Promise} resolved with the state of wkhtmltopdf on the server
* (possible values are 'ok', 'broken', 'install', 'upgrade', 'workers').
*/
checkWkhtmltopdf: function () {
diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js
index f09d8745f04..172148e03ef 100644
--- a/addons/web/static/src/js/tools/debug_manager.js
+++ b/addons/web/static/src/js/tools/debug_manager.js
@@ -62,7 +62,7 @@ var DebugManager = Widget.extend({
this._has_features = false;
// whether the current user is an administrator
this._is_admin = session.is_system;
- return $.when(
+ return Promise.resolve(
this._rpc({
model: 'res.users',
method: 'check_access_rights',
@@ -160,7 +160,7 @@ var DebugManager = Widget.extend({
.append(QWeb.render('WebClient.DebugManager.Global', {
manager: this,
}));
- return $.when();
+ return Promise.resolve();
},
select_view: function () {
var self = this;
@@ -281,7 +281,7 @@ DebugManager.include({
model: 'ir.model',
method: 'search',
args: [[['model', '=', model]]]
- }).done(function (ids) {
+ }).then(function (ids) {
self.do_action({
res_model: 'ir.model.fields',
name: _t('View Fields'),
@@ -324,7 +324,7 @@ DebugManager.include({
DebugManager.include({
start: function () {
this._can_edit_views = false;
- return $.when(
+ return Promise.all([
this._super(),
this._rpc({
model: 'ir.ui.view',
@@ -334,6 +334,7 @@ DebugManager.include({
.then(function (ar) {
this._can_edit_views = ar;
}.bind(this))
+ ]
);
},
update: function (tag, descriptor, widget) {
@@ -382,7 +383,7 @@ DebugManager.include({
model: this._action.res_model,
method: 'get_metadata',
args: [selectedIDs],
- }).done(function(result) {
+ }).then(function(result) {
var metadata = result[0];
metadata.creator = field_utils.format.many2one(metadata.create_uid);
metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid);
@@ -515,7 +516,7 @@ DebugManager.include({
true,
condition || false,
],
- }).done(function () { d.close(); });
+ }).then(function () { d.close(); });
}}
]
});
diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js
index 4e31b2d4467..d950d5d56ee 100644
--- a/addons/web/static/src/js/tools/test_menus.js
+++ b/addons/web/static/src/js/tools/test_menus.js
@@ -65,16 +65,16 @@
}
console.log('Found ', $listOfAppMenuItems.length, 'apps to test');
- var testDef = $.when();
- testDef = chainDeferred($listOfAppMenuItems, testDef, testApp);
- return testDef.then(function() {
+ var testPromise = Promise.resolve();
+ testPromise = chainDeferred($listOfAppMenuItems, testPromise, testApp);
+ return testPromise.then(function () {
console.log("Successfully tested ", testedApps.length, " apps");
console.log("Successfully tested ", testedMenus.length - testedApps.length, " menus");
console.log("test successful");
- }).always(function() {
- console.log("Test took ", (performance.now() - startTime)/1000, " seconds");
- }).fail(function () {
- console.error("test failed");
+ console.log("Test took ", (performance.now() - startTime) / 1000, " seconds");
+ }).catch(function () {
+ console.log("Test took ", (performance.now() - startTime) / 1000, " seconds");
+ console.error("Error !");
});
}
@@ -86,18 +86,18 @@
* 3 - clicking on each menu
* 3.1 - clicking on each view
* @param {DomElement} element: the App menu item
- * @returns {Deferred}
+ * @returns {Promise}
*/
function testApp(element){
console.log("Testing app menu:", element.dataset.menuXmlid);
- if (testedApps.indexOf(element.dataset.menuXmlid) >= 0) return $.Deferred().resolve(); // Another infinite loop protection
+ if (testedApps.indexOf(element.dataset.menuXmlid) >= 0) return Promise.resolve(); // Another infinite loop protection
testedApps.push(element.dataset.menuXmlid);
return testMenuItem(element).then(function () {
var $subMenuItems;
$subMenuItems = $('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4');
- var testMenuDef = $.when();
- testMenuDef = chainDeferred($subMenuItems, testMenuDef, testMenuItem);
- return testMenuDef;
+ var testMenuPromise = Promise.resolve();
+ testMenuPromise = chainDeferred($subMenuItems, testMenuPromise, testMenuItem);
+ return testMenuPromise;
}).then(function(){
// no effect in community
var $homeMenu = $("nav.o_main_navbar > a.o_menu_toggle.fa-th");
@@ -112,10 +112,10 @@
* 2 - Orchestrate the view switch
*
* @param {DomElement} element: the menu item
- * @returns {Deferred}
+ * @returns {Promise}
*/
function testMenuItem(element){
- if (testedMenus.indexOf(element.dataset.menuXmlid) >= 0) return $.Deferred().resolve(); // Avoid infinite loop
+ if (testedMenus.indexOf(element.dataset.menuXmlid) >= 0) return Promise.resolve(); // Avoid infinite loop
console.log("Testing menu", element.innerText.trim(), " ", element.dataset.menuXmlid);
testedMenus.push(element.dataset.menuXmlid);
var startActionCount = clientActionCount;
@@ -128,7 +128,7 @@
$modal.modal('hide');
isModal = true;
return true;
- };
+ }
return startActionCount != clientActionCount;
}).then(function() {
if (!isModal) {
@@ -138,7 +138,7 @@
if (!isModal) {
return testViews();
}
- }).fail(function() {
+ }).catch(function () {
console.error("Error while testing", element);
});
};
@@ -151,21 +151,21 @@
*/
function testViews() {
var $switches = $("nav.o_cp_switch_buttons > button:not(.active):visible");
- var testSwitchDef = $.when();
- // chainDeferred($switches, testSwitchDef, testViewSwitch # FIXME
+ var testSwitchPromise = Promise.resolve();
+ // chainDeferred($switches, testSwitchPromise, testViewSwitch # FIXME
_.each($switches, function(switchButton) {
- testSwitchDef = testSwitchDef.then(function () {
+ testSwitchPromise = testSwitchPromise.then(function () {
// get the view view-type data attribute
return testViewSwitch(switchButton.dataset.viewType);
});
});
- return testSwitchDef;
+ return testSwitchPromise;
}
/**
* Test a view button
* @param {string} viewType: a string for the view type to test (list, kanban ...)
- * @returns {Deferred} a deferred that wait for the view to be loaded
+ * @returns {Promise} a promise that wait for the view to be loaded
*/
function testViewSwitch(viewType){
console.log("Testing view switch: ", viewType);
@@ -188,20 +188,20 @@
* Click on each filter in the control pannel
*/
function testFilters() {
- var filterDef = $.when();
+ var filterProm = Promise.resolve();
// var $filters = $('div.o_control_panel div.btn-group.o_dropdown > ul.o_filters_menu > li:not(.o_add_custom_filter)');
var $filters = $('.o_filters_menu > .o_menu_item');
console.log("Testing " + $filters.length + " filters");
var filter_ids = _.compact(_.map($filters, function(f) { return f.dataset.id}));
_.each(filter_ids, function(filter_id){
- filterDef = filterDef.then(function(){
+ filterProm = filterProm.then(function(){
var currentViewCount = viewUpdateCount;
var $filter = $('.o_menu_item[data-id="' + filter_id + '"] a');
// with some customized search views, the filter cannot be found
if ($filter[0] === undefined) {
console.warn('Filter with ID ', filter_id , 'cannot be found');
- return $.Deferred().resolve();
- };
+ return Promise.resolve();
+ }
console.log('Clicking on filter "', $filter.text().trim(), '"');
$filter[0].click();
setTimeout(function() {
@@ -220,52 +220,55 @@
});
});
});
- return filterDef;
+ return filterProm;
}
// utility functions
/**
* Wait a certain amount of time for a condition to occur
- * @param stopCondition: a function that returns a boolean
- * @returns {Deferred} that is rejected if the timeout is exceeded
+ * @param {function} stopCondition a function that returns a boolean
+ * @returns {Promise} that is rejected if the timeout is exceeded
*/
function waitForCondition(stopCondition) {
- var def = $.Deferred();
- var interval = 250;
- var timeLimit = 15000;
- function checkCondition() {
- if (stopCondition()) {
- def.resolve();
- } else {
- timeLimit -= interval;
- if (timeLimit > 0) {
- // recursive call until the resolve or the timeout
- setTimeout(checkCondition, interval);
+ var prom = new Promise(function (resolve, reject) {
+ var interval = 250;
+ var timeLimit = 15000;
+
+ function checkCondition() {
+ if (stopCondition()) {
+ resolve();
} else {
- console.error("Timeout exceeded", stopCondition);
- def.reject();
+ timeLimit -= interval;
+ if (timeLimit > 0) {
+ // recursive call until the resolve or the timeout
+ setTimeout(checkCondition, interval);
+ } else {
+ console.error("Timeout exceeded", stopCondition);
+ reject();
+ }
}
}
- }
- setTimeout(checkCondition, interval);
- return def;
- };
+ setTimeout(checkCondition, interval);
+ });
+ return prom;
+ }
/**
- * chain deferred actions
- * @param $elements: a list of jquery elements to be passed as arg to the function
- * @param deferred: the deferred on which other deferreds will be chained
- * @param f: the function to be deferred
- * @returns : the chained deferred
+ * Chain deferred actions.
+ *
+ * @param {jQueryElement} $elements a list of jquery elements to be passed as arg to the function
+ * @param {Promise} promise the promise on which other promises will be chained
+ * @param {function} f the function to be deferred
+ * @returns {Promise} the chained promise
*/
- function chainDeferred($elements, deferred, f) {
+ function chainDeferred($elements, promise, f) {
_.each($elements, function(el) {
- deferred = deferred.then(function () {
+ promise = promise.then(function () {
return f(el);
});
});
- return deferred;
+ return promise;
}
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index b5d7c8fe8b3..0c251874480 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -62,7 +62,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
/**
* Simply renders and updates the url.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
var self = this;
@@ -70,12 +70,16 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
this.$el.addClass('o_view_controller');
return this._super.apply(this, arguments).then(function () {
+ var prom;
if (self._controlPanel) {
// render the ControlPanel elements (buttons, pager, sidebar...)
- self.controlPanelElements = self._renderControlPanelElements();
- self._controlPanel.$el.prependTo(self.$el);
+ prom = self._renderControlPanelElements().then(function (elements) {
+ self.controlPanelElements = elements;
+ self._controlPanel.$el.prependTo(self.$el);
+ });
}
-
+ return Promise.resolve(prom);
+ }).then(function () {
return self._update(self.initialState);
});
},
@@ -119,7 +123,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*/
canBeRemoved: function () {
// AAB: get rid of this option when on_hashchange mechanism is improved
- return this.discardChanges(undefined, {readonlyIfRealDiscard: true});
+ return this.discardChanges(undefined, { readonlyIfRealDiscard: true });
},
/**
* Discards the changes made on the record associated to the given ID, or
@@ -128,15 +132,15 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* this method on the active view to make sure it is ok to open the home
* screen (and lose all current state).
*
- * Note that it returns a deferred, because the view could choose to ask the
+ * Note that it returns a Promise, because the view could choose to ask the
* user if he agrees to discard.
*
* @param {string} [recordID]
* if not given, we consider all the changes made by the controller
- * @returns {Deferred} resolved if properly discarded, rejected otherwise
+ * @returns {Promise} resolved if properly discarded, rejected otherwise
*/
discardChanges: function (recordID) {
- return $.when();
+ return Promise.resolve();
},
/**
* Export the state of the controller containing information that is shared
@@ -155,7 +159,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
/**
* Gives the focus to the renderer
*/
- giveFocus: function() {
+ giveFocus: function () {
this.renderer.giveFocus();
},
/**
@@ -174,7 +178,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* Short helper method to reload the view
*
* @param {Object} [params] This object will simply be given to the update
- * @returns {Deferred}
+ * @returns {Promise}
*/
reload: function (params) {
params = params || {};
@@ -186,7 +190,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
params = _.extend({}, params, searchQuery);
});
}
- return $.when(def).then(this.update.bind(this, params, {}));
+ return Promise.resolve(def).then(this.update.bind(this, params, {}));
},
/**
* For views that require a pager, this method will be called to allow the
@@ -195,16 +199,20 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* your view does not want a pager, just let this method empty.
*
* @param {jQuery Node} $node
+ * @return {Promise}
*/
renderPager: function ($node) {
+ return Promise.resolve();
},
/**
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu
* dropdown in the control panel next to the buttons)
*
* @param {jQuery Node} $node
+ * @return {Promise}
*/
renderSidebar: function ($node) {
+ return Promise.resolve();
},
/**
* This is the main entry point for the controller. Changes from the search
@@ -219,12 +227,12 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* @param {Object} [options]
* @param {boolean} [options.reload=true] if true, the model will reload data
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
update: function (params, options) {
var self = this;
var shouldReload = (options && 'reload' in options) ? options.reload : true;
- var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
+ var def = shouldReload ? this.model.reload(this.handle, params) : Promise.resolve();
// we check here that the updateIndex of the control panel hasn't changed
// between the start of the update request and the moment the controller
// asks the control panel to update itself ; indeed, it could happen that
@@ -285,17 +293,17 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* return {'html': '<h1>hello, world</h1>'}
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderBanner: function () {
if (this.bannerRoute !== undefined) {
var self = this;
return this.dp
- .add(this._rpc({route: this.bannerRoute}))
+ .add(this._rpc({ route: this.bannerRoute }))
.then(function (response) {
if (!response.html) {
self.$el.removeClass('o_has_banner');
- return $.when();
+ return Promise.resolve();
}
self.$el.addClass('o_has_banner');
var $banner = $(response.html);
@@ -313,22 +321,24 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
defs.push(ajax.loadJS(js.src));
js.remove();
});
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
$banner.prependTo(self.$('> .o_content'));
self._$banner = $banner;
});
});
}
- return $.when();
+ return Promise.resolve();
},
/**
* Renders the control elements (buttons, pager and sidebar) of the current
* view.
*
* @private
- * @returns {Object} an object containing the control panel jQuery elements
+ * @returns {Promise<Object>} resolved with an object containing the control
+ * panel jQuery elements
*/
_renderControlPanelElements: function () {
+ var self = this;
var elements = {
$buttons: $('<div>'),
$sidebar: $('<div>'),
@@ -336,15 +346,18 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
};
this.renderButtons(elements.$buttons);
- this.renderSidebar(elements.$sidebar);
- this.renderPager(elements.$pager);
- // remove the unnecessary outer div
- elements = _.mapObject(elements, function($node) {
- return $node && $node.contents();
- });
- elements.$switch_buttons = this._renderSwitchButtons();
+ var sidebarProm = this.renderSidebar(elements.$sidebar);
+ var pagerProm = this.renderPager(elements.$pager);
- return elements;
+ return Promise.all([sidebarProm, pagerProm]).then(function () {
+ // remove the unnecessary outer div
+ elements = _.mapObject(elements, function ($node) {
+ return $node && $node.contents();
+ });
+ elements.$switch_buttons = self._renderSwitchButtons();
+
+ return elements;
+ });
},
/**
* Renders the switch buttons and binds listeners on them.
@@ -354,7 +367,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*/
_renderSwitchButtons: function () {
var self = this;
- var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});
+ var views = _.filter(this.actionViews, { multiRecord: this.isMultiRecord });
if (views.length <= 1) {
return $();
@@ -373,7 +386,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button');
$switchButtonsFiltered.click(_.debounce(function (event) {
var viewType = $(event.target).data('view-type');
- self.trigger_up('switch_view', {view_type: viewType});
+ self.trigger_up('switch_view', { view_type: viewType });
}, 200, true));
// set active view's icon as view switcher button's icon in mobile
@@ -402,7 +415,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*
* @private
* @param {Object} state the state given by the model
- * @returns {Deferred}
+ * @returns {Promise}
*/
_update: function () {
// AAB: update the control panel -> this will be moved elsewhere at some point
@@ -524,7 +537,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*/
_onOpenRecord: function (ev) {
ev.stopPropagation();
- var record = this.model.get(ev.data.id, {raw: true});
+ var record = this.model.get(ev.data.id, { raw: true });
this.trigger_up('switch_view', {
view_type: 'form',
res_id: record.res_id,
diff --git a/addons/web/static/src/js/views/abstract_model.js b/addons/web/static/src/js/views/abstract_model.js
index da73ac664a3..d18367b1756 100644
--- a/addons/web/static/src/js/views/abstract_model.js
+++ b/addons/web/static/src/js/views/abstract_model.js
@@ -31,7 +31,7 @@ var AbstractModel = mvc.Model.extend({
* @returns {Promise}
*/
reload: function (params) {
- return $.when();
+ return Promise.resolve();
},
/**
* Processes date(time) and selection field values sent by the server.
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 0f18263c9ee..724b63bae79 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -23,14 +23,14 @@ return mvc.Renderer.extend({
this.noContentHelp = params.noContentHelp;
},
/**
- * The rendering can be asynchronous (but it is not encouraged). The start
+ * The rendering is asynchronous. The start
* method simply makes sure that we render the view.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this.$el.addClass(this.arch.attrs.class);
- return $.when(this._render(), this._super());
+ return Promise.all([this._render(), this._super()]);
},
/**
* Called each time the renderer is attached into the DOM.
@@ -86,11 +86,11 @@ return mvc.Renderer.extend({
* @param {Object} params
* @param {boolean} [params.noRender=false]
* if true, the method only updates the state without rerendering
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateState: function (state, params) {
this.state = state;
- return params.noRender ? $.when() : this._render();
+ return params.noRender ? Promise.resolve() : this._render();
},
//--------------------------------------------------------------------------
@@ -102,10 +102,10 @@ return mvc.Renderer.extend({
*
* @abstract
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
- return $.when();
+ return Promise.resolve();
},
});
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index ce8951f6ee8..ba3c198bd21 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -194,11 +194,12 @@ var AbstractView = Factory.extend({
def = this._createControlPanel(parent);
}
var _super = this._super.bind(this);
- return $.when(def).then(function (controlPanel) {
+ return Promise.resolve(def).then(function (controlPanel) {
// get the parent of the model if it already exists, as _super will
// set the new controller as parent, which we don't want
var modelParent = self.model && self.model.getParent();
- return _super(parent).done(function (controller) {
+ var prom = _super(parent);
+ prom.then(function (controller) {
if (controlPanel) {
controlPanel.setParent(controller);
}
@@ -207,7 +208,7 @@ var AbstractView = Factory.extend({
self.model.setParent(modelParent);
}
});
-
+ return prom;
});
},
/**
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,
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 4d86ef4293b..95b6d439ec1 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -173,7 +173,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string} [options.position=top] if the new record should be added
* on top or on bottom of the list
- * @returns {Deferred<string>} resolves to the id of the new created record
+ * @returns {Promise<string>} resolves to the id of the new created record
*/
addDefaultRecord: function (listID, options) {
var self = this;
@@ -241,7 +241,7 @@ var BasicModel = AbstractModel.extend({
* assume main viewType from the record
* @param {Array} [options.fieldNames] list of field names for which a
* default value must be generated (used to complete the values dict)
- * @returns {Deferred}
+ * @returns {Promise}
*/
applyDefaultValues: function (recordID, values, options) {
options = options || {};
@@ -304,7 +304,7 @@ var BasicModel = AbstractModel.extend({
}
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Onchange RPCs may return values for fields that are not in the current
@@ -321,7 +321,7 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} recordID local resource id of a record
* @param {string} viewType the current viewType
- * @returns {Deferred<string>} resolves to the id of the record
+ * @returns {Promise<string>} resolves to the id of the record
*/
applyRawChanges: function (recordID, viewType) {
var record = this.localData[recordID];
@@ -388,7 +388,7 @@ var BasicModel = AbstractModel.extend({
* @param {string[]} recordIds list of local resources ids. They should all
* be of type 'record', be of the same model and have the same parent.
* @param {string} modelName mode name used to unlink the records
- * @returns {Deferred}
+ * @returns {Promise}
*/
deleteRecords: function (recordIds, modelName) {
var self = this;
@@ -457,7 +457,7 @@ var BasicModel = AbstractModel.extend({
* Duplicate a record (by calling the 'copy' route)
*
* @param {string} recordID id for a local resource
- * @returns {Deferred<string>} resolves to the id of duplicate record
+ * @returns {Promise<string>} resolves to the id of duplicate record
*/
duplicateRecord: function (recordID) {
var self = this;
@@ -725,7 +725,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} params.fields contains the description of each field
* @param {string} [params.type] 'record' or 'list'
* @param {string} [params.recordID] an ID for an existing resource.
- * @returns {Deferred<string>} resolves to a local id, or handle
+ * @returns {Promise<string>} resolves to a local id, or handle
*/
load: function (params) {
params.type = params.type || (params.res_id !== undefined ? 'record' : 'list');
@@ -758,7 +758,7 @@ var BasicModel = AbstractModel.extend({
* @param {string} model name of the model
* @param {Object[]} fields a description of field properties
* @param {Object} [fieldInfo] various field info that we want to set
- * @returns {string} the local id for the created resource
+ * @returns {Promise<string>} the local id for the created resource
*/
makeRecord: function (model, fields, fieldInfo) {
var self = this;
@@ -849,7 +849,7 @@ var BasicModel = AbstractModel.extend({
record.data[field.name] = field.value;
}
});
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
return record.id;
});
},
@@ -863,7 +863,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} changes a map field => new value
* @param {Object} [options] will be transferred to the applyChange method
* @see _applyChange
- * @returns {string[]} list of changed fields
+ * @returns {Promise<string[]>} list of changed fields
*/
notifyChanges: function (record_id, changes, options) {
return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
@@ -876,7 +876,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {boolean} [options.keepChanges=false] if true, doesn't discard the
* changes on the record before reloading it
- * @returns {Deferred<string>} resolves to the id of the resource
+ * @returns {Promise<string>} resolves to the id of the resource
*/
reload: function (id, options) {
return this.mutex.exec(this._reload.bind(this, id, options));
@@ -925,12 +925,12 @@ var BasicModel = AbstractModel.extend({
* @param {object} [options]
* @param {integer} [options.offset]
* @param {string} [options.field] the field name used as sequence
- * @returns {Deferred<string>} resolves to the local id of the parent
+ * @returns {Promise<string>} resolves to the local id of the parent
*/
resequence: function (modelName, resIDs, parentID, options) {
options = options || {};
if ((resIDs.length <= 1)) {
- return $.when(parentID); // there is nothing to sort
+ return Promise.resolve(parentID); // there is nothing to sort
}
var self = this;
var data = this.localData[parentID];
@@ -952,7 +952,7 @@ var BasicModel = AbstractModel.extend({
if (!wasResequenced) {
// the field on which the resequence was triggered does not
// exist, so no resequence happened server-side
- return $.when();
+ return Promise.resolve();
}
var field = params.field ? params.field : 'sequence';
@@ -1010,7 +1010,7 @@ var BasicModel = AbstractModel.extend({
* be restored later by call discardChanges with option rollback to true
* @param {string} [options.viewType] current viewType. If not set, we will
* assume main viewType from the record
- * @returns {Deferred}
+ * @returns {Promise}
* Resolved with the list of field names (whose value has been modified)
*/
save: function (recordID, options) {
@@ -1054,57 +1054,59 @@ var BasicModel = AbstractModel.extend({
});
}
- var def = $.Deferred();
- var changedFields = Object.keys(changes);
+ var prom = new Promise(function (resolve, reject) {
+ var changedFields = Object.keys(changes);
- if (options.savePoint) {
- return def.resolve(changedFields);
- }
+ if (options.savePoint) {
+ resolve(changedFields);
+ return;
+ }
- def.then(function () {
- record._isDirty = false;
- });
- // in the case of a write, only perform the RPC if there are changes to save
- if (method === 'create' || changedFields.length) {
- var args = method === 'write' ? [[record.data.id], changes] : [changes];
- self._rpc({
- model: record.model,
- method: method,
- args: args,
- context: record.getContext(),
- }).then(function (id) {
- if (method === 'create') {
- record.res_id = id; // create returns an id, write returns a boolean
- record.data.id = id;
- record.offset = record.res_ids.length;
- record.res_ids.push(id);
- record.count++;
- }
+ // in the case of a write, only perform the RPC if there are changes to save
+ if (method === 'create' || changedFields.length) {
+ var args = method === 'write' ? [[record.data.id], changes] : [changes];
+ self._rpc({
+ model: record.model,
+ method: method,
+ args: args,
+ context: record.getContext(),
+ }).then(function (id) {
+ if (method === 'create') {
+ record.res_id = id; // create returns an id, write returns a boolean
+ record.data.id = id;
+ record.offset = record.res_ids.length;
+ record.res_ids.push(id);
+ record.count++;
+ }
- var _changes = record._changes;
+ var _changes = record._changes;
- // Erase changes as they have been applied
- record._changes = {};
+ // Erase changes as they have been applied
+ record._changes = {};
- // Optionally clear the DataManager's cache
- self._invalidateCache(record);
+ // Optionally clear the DataManager's cache
+ self._invalidateCache(record);
- self.unfreezeOrder(record.id);
+ self.unfreezeOrder(record.id);
- // Update the data directly or reload them
- if (shouldReload) {
- self._fetchRecord(record).then(function () {
- def.resolve(changedFields);
- });
- } else {
- _.extend(record.data, _changes);
- def.resolve(changedFields);
- }
- }).fail(def.reject.bind(def));
- } else {
- def.resolve(changedFields);
- }
- return def;
+ // Update the data directly or reload them
+ if (shouldReload) {
+ self._fetchRecord(record).then(function () {
+ resolve(changedFields);
+ });
+ } else {
+ _.extend(record.data, _changes);
+ resolve(changedFields);
+ }
+ }).guardedCatch(reject);
+ } else {
+ resolve(changedFields);
+ }
+ });
+ prom.then(function () {
+ record._isDirty = false;
+ });
+ return prom;
});
},
/**
@@ -1123,7 +1125,7 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} list_id id for the list resource
* @param {string} fieldName valid field name
- * @returns {Deferred}
+ * @returns {Promise}
*/
setSort: function (list_id, fieldName) {
var list = this.localData[list_id];
@@ -1153,7 +1155,7 @@ var BasicModel = AbstractModel.extend({
// sort field is still unknown (i.e. on other pages for example)
return this._fetchUngroupedList(list);
}
- return $.when();
+ return Promise.resolve();
},
/**
* Toggle the active value of given records (to archive/unarchive them)
@@ -1161,7 +1163,7 @@ var BasicModel = AbstractModel.extend({
* @param {Array} recordIDs local ids of the records to (un)archive
* @param {boolean} value false to archive, true to unarchive (value of the active field)
* @param {string} parentID id of the parent resource to reload
- * @returns {Deferred<string>} resolves to the parent id
+ * @returns {Promise<string>} resolves to the parent id
*/
toggleActive: function (recordIDs, value, parentID) {
var self = this;
@@ -1185,7 +1187,7 @@ var BasicModel = AbstractModel.extend({
* data
*
* @param {string} groupId
- * @returns {Deferred<string>} resolves to the group id
+ * @returns {Promise<string>} resolves to the group id
*/
toggleGroup: function (groupId) {
var self = this;
@@ -1196,7 +1198,7 @@ var BasicModel = AbstractModel.extend({
group.res_ids = [];
group.offset = 0;
this._updateParentResIDs(group);
- return $.when(groupId);
+ return Promise.resolve(groupId);
}
if (!group.isOpen) {
group.isOpen = true;
@@ -1206,7 +1208,7 @@ var BasicModel = AbstractModel.extend({
self._updateParentResIDs(group);
});
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
return groupId;
});
}
@@ -1260,7 +1262,7 @@ var BasicModel = AbstractModel.extend({
* If several contexts are found, multiple records are added
* @param {boolean} [options.allowWarning=false] if true, the default record
* operation can complete, even if a warning is raised
- * @returns {Deferred<[string]>} resolves to the new records ids
+ * @returns {Promise<[string]>} resolves to the new records ids
*/
_addX2ManyDefaultRecord: function (list, options) {
var self = this;
@@ -1286,9 +1288,9 @@ var BasicModel = AbstractModel.extend({
makeDefaultRecords.push(self._makeDefaultRecord(list.model, params));
}
- return $.when.apply($, makeDefaultRecords).then(function (){
+ return Promise.all(makeDefaultRecords).then(function (resultIds){
var ids = [];
- _.each(arguments, function (id){
+ _.each(resultIds, function (id){
ids.push(id);
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
@@ -1324,7 +1326,7 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [options.allowWarning=false] if true, change
* operation can complete, even if a warning is raised
* (only supported by X2ManyChange)
- * @returns {Deferred} list of changed fields
+ * @returns {Promise} list of changed fields
*/
_applyChange: function (recordID, changes, options) {
var self = this;
@@ -1354,10 +1356,10 @@ var BasicModel = AbstractModel.extend({
}
if (options.notifyChange === false) {
- return $.Deferred().resolve(_.keys(changes));
+ return Promise.resolve(_.keys(changes));
}
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
var onChangeFields = []; // the fields that have changed and that have an on_change
for (var fieldName in changes) {
field = record.fields[fieldName];
@@ -1368,22 +1370,22 @@ var BasicModel = AbstractModel.extend({
}
}
}
- var onchangeDef = $.Deferred();
- if (onChangeFields.length) {
- self._performOnChange(record, onChangeFields, options.viewType)
+ return new Promise(function (resolve, reject) {
+ if (onChangeFields.length) {
+ self._performOnChange(record, onChangeFields, options.viewType)
.then(function (result) {
delete record._warning;
- onchangeDef.resolve(_.keys(changes).concat(Object.keys(result && result.value || {})));
- }).fail(function () {
+ resolve(_.keys(changes).concat(Object.keys(result && result.value || {})));
+ }).guardedCatch(function () {
self._visitChildren(record, function (elem) {
_.extend(elem, initialData[elem.id]);
});
- onchangeDef.resolve({});
+ resolve({});
});
- } else {
- onchangeDef = $.Deferred().resolve(_.keys(changes));
- }
- return onchangeDef.then(function (fieldNames) {
+ } else {
+ resolve(_.keys(changes));
+ }
+ }).then(function (fieldNames) {
_.each(fieldNames, function (name) {
if (record._changes && record._changes[name] === record.data[name]) {
delete record._changes[name];
@@ -1407,13 +1409,13 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record
* @param {string} fieldName
* @param {Object} [data]
- * @returns {Deferred}
+ * @returns {Promise}
*/
_applyX2OneChange: function (record, fieldName, data) {
var self = this;
if (!data || !data.id) {
record._changes[fieldName] = false;
- return $.when();
+ return Promise.resolve();
}
// here, we check that the many2one really changed. If the res_id is the
@@ -1429,7 +1431,7 @@ var BasicModel = AbstractModel.extend({
}
var relatedRecord = this.localData[relatedID];
if (relatedRecord && (data.id === this.localData[relatedID].res_id)) {
- return $.when();
+ return Promise.resolve();
}
var rel_data = _.pick(data, 'id', 'display_name');
var field = record.fields[fieldName];
@@ -1450,7 +1452,7 @@ var BasicModel = AbstractModel.extend({
rel_data.display_name = result[0][1];
});
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
var rec = self._makeDataPoint({
context: record.context,
data: rel_data,
@@ -1471,7 +1473,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_applyOnChange: function (values, record, viewType) {
var self = this;
@@ -1634,7 +1636,7 @@ var BasicModel = AbstractModel.extend({
var def = self._readUngroupedList(list).then(function () {
var x2ManysDef = self._fetchX2ManysBatched(list);
var referencesDef = self._fetchReferencesBatched(list);
- return $.when(x2ManysDef, referencesDef);
+ return Promise.all([x2ManysDef, referencesDef]);
});
defs.push(def);
} else {
@@ -1644,7 +1646,7 @@ var BasicModel = AbstractModel.extend({
}
}
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
// inner function that adds a record (based on its res_id) to a list
// dataPoint (used for onchanges that return commands 4 (LINK TO) or
@@ -1680,7 +1682,7 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [allowWarning=false] if true, change
* operation can complete, even if a warning is raised
* (only supported by the 'CREATE' command.operation)
- * @returns {Deferred}
+ * @returns {Promise}
*/
_applyX2ManyChange: function (record, fieldName, command, viewType, allowWarning) {
if (command.operation === 'TRIGGER_ONCHANGE') {
@@ -1688,7 +1690,7 @@ var BasicModel = AbstractModel.extend({
// there is no need to apply any change on the record (the changes
// have probably been already applied and saved, usecase: many2many
// edition in a dialog)
- return $.when();
+ return Promise.resolve();
}
var self = this;
@@ -1765,10 +1767,10 @@ var BasicModel = AbstractModel.extend({
list_records[record.id].data = record;
self._parseServerData(fieldNames, list, record);
});
- return $.when(
+ return Promise.all([
self._fetchX2ManysBatched(list),
self._fetchReferencesBatched(list)
- );
+ ]);
});
defs.push(def);
}
@@ -1850,10 +1852,10 @@ var BasicModel = AbstractModel.extend({
}),
}, viewType);
}
- return $.when(addDef, removedDef);
+ return Promise.all([addDef, removedDef]);
}
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
// ensure to fetch up to 'limit' records (may be useful if records of
// the current page have been removed)
return self._readUngroupedList(list).then(function () {
@@ -2103,7 +2105,7 @@ var BasicModel = AbstractModel.extend({
* Fetch all name_gets for the many2ones in a group
*
* @param {Object[]} groups a list of object with context and record sub keys
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchMany2OneGroup: function (groups) {
var ids = _.uniq(_.pluck(_.pluck(groups, 'record'), 'res_id'));
@@ -2124,7 +2126,7 @@ var BasicModel = AbstractModel.extend({
* Fetch name_get for a record datapoint.
*
* @param {Object} dataPoint
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchNameGet: function (dataPoint) {
return this._rpc({
@@ -2143,7 +2145,7 @@ var BasicModel = AbstractModel.extend({
* @params {Object} list: must be a datapoint of type list
* (for example: a datapoint representing a x2many)
* @params {string} fieldName: the name of a field of type Many2one or Reference
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchNameGets: function (list, fieldName) {
var self = this;
@@ -2168,7 +2170,7 @@ var BasicModel = AbstractModel.extend({
});
if (!ids.length) {
- return $.when();
+ return Promise.resolve();
}
return this._rpc({
model: model,
@@ -2195,7 +2197,7 @@ var BasicModel = AbstractModel.extend({
* @param {string} [options.viewType] the type of view for which the record
* is fetched (usefull to load the adequate fields), by defaults, uses
* record.viewType
- * @returns {Deferred<Object>} resolves to the record or is rejected in
+ * @returns {Promise<Object>} resolves to the record or is rejected in
* case no id given were valid ids
*/
_fetchRecord: function (record, options) {
@@ -2211,7 +2213,7 @@ var BasicModel = AbstractModel.extend({
})
.then(function (result) {
if (result.length === 0) {
- return $.Deferred().reject();
+ return Promise.reject();
}
result = result[0];
record.data = _.extend({}, record.data, result);
@@ -2220,10 +2222,10 @@ var BasicModel = AbstractModel.extend({
self._parseServerData(fieldNames, record, record.data);
})
.then(function () {
- return $.when(
+ return Promise.all([
self._fetchX2Manys(record, options),
self._fetchReferences(record, options)
- ).then(function () {
+ ]).then(function () {
return self._postprocess(record, options);
});
});
@@ -2234,7 +2236,7 @@ var BasicModel = AbstractModel.extend({
* @private
* @param {Object} record
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReference: function (record, fieldName) {
var self = this;
@@ -2259,7 +2261,7 @@ var BasicModel = AbstractModel.extend({
});
});
}
- return $.when(def);
+ return Promise.resolve(def);
},
/**
* Fetches data for reference fields and assigns these data to newly
@@ -2270,7 +2272,7 @@ var BasicModel = AbstractModel.extend({
* @see _getDataToFetchByModel
* @param {string} model
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferenceData: function (datapoints, model, fieldName) {
var self = this;
@@ -2307,7 +2309,7 @@ var BasicModel = AbstractModel.extend({
*
* @private
* @param {Object} record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferences: function (record, options) {
var self = this;
@@ -2324,7 +2326,7 @@ var BasicModel = AbstractModel.extend({
defs.push(def);
}
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Batch requests for one reference field in list (one request by different
@@ -2333,7 +2335,7 @@ var BasicModel = AbstractModel.extend({
* @see _fetchReferencesBatched
* @param {Object} list
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferenceBatched: function (list, fieldName) {
var self = this;
@@ -2347,13 +2349,13 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchReferenceData(datapoints, model, fieldName));
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Batch requests for references for datapoint of type list.
*
* @param {Object} list
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferencesBatched: function (list) {
var defs = [];
@@ -2364,7 +2366,7 @@ var BasicModel = AbstractModel.extend({
defs.push(this._fetchReferenceBatched(list, fieldNames[i]));
}
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Batch reference requests for all records in list.
@@ -2372,7 +2374,7 @@ var BasicModel = AbstractModel.extend({
* @see _fetchReferencesSingleBatch
* @param {Object} list a valid resource object
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferenceSingleBatch: function (list, fieldName) {
var self = this;
@@ -2390,14 +2392,14 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchReferenceData(datapoints, model, fieldName));
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Batch requests for all reference field in list's children.
* Called by _readGroup to make only one 'name_get' rpc by fieldName.
*
* @param {Object} list a valid resource object
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchReferencesSingleBatch: function (list) {
var defs = [];
@@ -2408,7 +2410,7 @@ var BasicModel = AbstractModel.extend({
defs.push(this._fetchReferenceSingleBatch(list, fieldNames[fIndex]));
}
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Fetch model data from server, relationally to fieldName and resulted
@@ -2420,7 +2422,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} toFetch a list of records and res_ids,
* @see _getDataToFetch
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchRelatedData: function (list, toFetch, fieldName) {
var self = this;
@@ -2431,7 +2433,7 @@ var BasicModel = AbstractModel.extend({
var fieldInfo = list.fieldsInfo[list.viewType][fieldName];
if (!ids.length || fieldInfo.__no_fetch) {
- return $.when();
+ return Promise.resolve();
}
var def;
@@ -2445,7 +2447,7 @@ var BasicModel = AbstractModel.extend({
context: list.getContext() || {},
});
} else {
- def = $.when(_.map(ids, function (id) {
+ def = Promise.resolve(_.map(ids, function (id) {
return {id:id};
}));
}
@@ -2465,10 +2467,10 @@ var BasicModel = AbstractModel.extend({
* This method is currently only called by _makeDefaultRecord, it should be
* called by the onchange methods at some point.
*
- * @todo fix bug: returns a list of deferred, not a deferred
+ * @todo fix bug: returns a list of promise, not a promise
*
* @param {Object} record a valid resource object
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchRelationalData: function (record) {
var self = this;
@@ -2496,7 +2498,7 @@ var BasicModel = AbstractModel.extend({
return [elem.record.model, JSON.stringify(elem.context)].join();
});
- return $.when.apply($, _.map(groups, this._fetchMany2OneGroup.bind(this)));
+ return Promise.all(_.map(groups, this._fetchMany2OneGroup.bind(this)));
},
/**
* Check the AbstractField specializations that are (will be) used by the
@@ -2507,15 +2509,15 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} record - an element from the localData
* @param {Object} options
- * @returns {Deferred<Array>}
- * The deferred is resolved with an array containing the names of
+ * @returns {Promise<Array>}
+ * The promise is resolved with an array containing the names of
* the field whose special data has been changed.
*/
_fetchSpecialData: function (record, options) {
var self = this;
var specialFieldNames = [];
var fieldNames = (options && options.fieldNames) || record.getFieldNames();
- return $.when.apply($, _.map(fieldNames, function (name) {
+ return Promise.all(_.map(fieldNames, function (name) {
var viewType = (options && options.viewType) || record.viewType;
var fieldInfo = record.fieldsInfo[viewType][name] || {};
var Widget = fieldInfo.Widget;
@@ -2541,16 +2543,16 @@ var BasicModel = AbstractModel.extend({
* @param {Object} fieldInfo
* @param {string[]} [fieldsToRead] - the m2os fields to read (id and
* display_name are automatic).
- * @returns {Deferred<any>}
- * The deferred is resolved with the fetched special data. If this
+ * @returns {Promise<any>}
+ * The promise is resolved with the fetched special data. If this
* data is the same as the previously fetched one (for the given
- * parameters), no RPC is done and the deferred is resolved with
+ * parameters), no RPC is done and the promise is resolved with
* the undefined value.
*/
_fetchSpecialMany2ones: function (record, fieldName, fieldInfo, fieldsToRead) {
var field = record.fields[fieldName];
if (field.type !== "many2one") {
- return $.when();
+ return Promise.resolve();
}
var context = record.getContext({fieldName: fieldName});
@@ -2571,7 +2573,7 @@ var BasicModel = AbstractModel.extend({
domain: domain,
});
if (!hasChanged) {
- return $.when();
+ return Promise.resolve();
}
var self = this;
@@ -2607,16 +2609,16 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} record - an element from the localData
* @param {Object} fieldName - the name of the field
- * @returns {Deferred<any>}
- * The deferred is resolved with the fetched special data. If this
+ * @returns {Promise<any>}
+ * The promise is resolved with the fetched special data. If this
* data is the same as the previously fetched one (for the given
- * parameters), no RPC is done and the deferred is resolved with
+ * parameters), no RPC is done and the promise is resolved with
* the undefined value.
*/
_fetchSpecialRelation: function (record, fieldName) {
var field = record.fields[fieldName];
if (!_.contains(["many2one", "many2many", "one2many"], field.type)) {
- return $.when();
+ return Promise.resolve();
}
var context = record.getContext({fieldName: fieldName});
@@ -2628,7 +2630,7 @@ var BasicModel = AbstractModel.extend({
domain: domain,
});
if (!hasChanged) {
- return $.when();
+ return Promise.resolve();
}
return this._rpc({
@@ -2645,7 +2647,7 @@ var BasicModel = AbstractModel.extend({
* @private
* @param {Object} record - an element from the localData
* @param {Object} fieldName - the name of the field
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchSpecialReference: function (record, fieldName) {
var def;
@@ -2655,7 +2657,7 @@ var BasicModel = AbstractModel.extend({
// needs to be fetched a posteriori
def = this._fetchReference(record, fieldName);
}
- return $.when(def);
+ return Promise.resolve(def);
},
/**
* Fetches all the m2o records associated to the given fieldName. If the
@@ -2666,10 +2668,10 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record - an element from the localData
* @param {Object} fieldName - the name of the field
* @param {Object} fieldInfo
- * @returns {Deferred<any>}
- * The deferred is resolved with the fetched special data. If this
+ * @returns {Promise<any>}
+ * The promise is resolved with the fetched special data. If this
* data is the same as the previously fetched one (for the given
- * parameters), no RPC is done and the deferred is resolved with
+ * parameters), no RPC is done and the promise is resolved with
* the undefined value.
*/
_fetchSpecialStatus: function (record, fieldName, fieldInfo) {
@@ -2689,13 +2691,14 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record - an element from the localData
* @param {Object} fieldName - the name of the field
* @param {Object} fieldInfo
- * @returns {Deferred<any>}
- * The deferred is resolved with the fetched special data. If this
+ * @returns {Promise<any>}
+ * The promise is resolved with the fetched special data. If this
* data is the same as the previously fetched one (for the given
- * parameters), no RPC is done and the deferred is resolved with
+ * parameters), no RPC is done and the promise is resolved with
* the undefined value.
*/
_fetchSpecialDomain: function (record, fieldName, fieldInfo) {
+ var self = this;
var context = record.getContext({fieldName: fieldName});
var domainModel = fieldInfo.options.model;
@@ -2711,34 +2714,37 @@ var BasicModel = AbstractModel.extend({
domainValue: domainValue,
});
if (!hasChanged) {
- return $.when();
+ return Promise.resolve();
} else if (!domainModel) {
- return $.when({
+ return Promise.resolve({
model: domainModel,
nbRecords: 0,
});
}
- var def = $.Deferred();
- var evalContext = this._getEvalContext(record);
- this._rpc({
+ return new Promise(function (resolve) {
+ var evalContext = self._getEvalContext(record);
+ self._rpc({
model: domainModel,
method: 'search_count',
args: [Domain.prototype.stringToArray(domainValue, evalContext)],
context: context
})
- .then(_.identity, function (error, e) {
- e.preventDefault(); // prevent traceback (the search_count might be intended to break)
- return false;
- })
- .always(function (nbRecords) {
- def.resolve({
+ .then(function (nbRecords) {
+ resolve({
model: domainModel,
nbRecords: nbRecords,
});
+ })
+ .guardedCatch(function (reason) {
+ var e = reason.event;
+ e.preventDefault(); // prevent traceback (the search_count might be intended to break)
+ resolve({
+ model: domainModel,
+ nbRecords: 0,
+ });
});
-
- return def;
+ });
},
/**
* Fetch all data in a ungrouped list
@@ -2748,7 +2754,7 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [options.enableRelationalFetch=true] if false, will not
* fetch x2m and relational data (that will be done by _readGroup in this
* case).
- * @returns {Deferred<Object>} resolves to the fetched list
+ * @returns {Promise<Object>} resolves to the fecthed list
*/
_fetchUngroupedList: function (list, options) {
options = _.defaults(options || {}, {enableRelationalFetch: true});
@@ -2762,7 +2768,7 @@ var BasicModel = AbstractModel.extend({
var defs = _.map(many2ones, function (name) {
return self._fetchNameGets(list, name);
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
}
});
} else {
@@ -2770,10 +2776,10 @@ var BasicModel = AbstractModel.extend({
}
return def.then(function () {
if (options.enableRelationalFetch) {
- return $.when(
- self._fetchX2ManysBatched(list, options),
- self._fetchReferencesBatched(list, options)
- );
+ return Promise.all([
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list)
+ ]);
}
}).then(function () {
return list;
@@ -2785,7 +2791,7 @@ var BasicModel = AbstractModel.extend({
* @see _fetchX2ManysBatched
* @param {Object} list
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchX2ManyBatched: function (list, fieldName) {
list = this._applyX2ManyOperations(list);
@@ -2810,7 +2816,7 @@ var BasicModel = AbstractModel.extend({
* @param {string} [options.viewType] the type of view for which the main
* record is fetched (useful to load the adequate fields), by defaults,
* uses record.viewType
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchX2Manys: function (record, options) {
var self = this;
@@ -2845,22 +2851,22 @@ var BasicModel = AbstractModel.extend({
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
var def = self._readUngroupedList(list).then(function () {
- return $.when(
+ return Promise.all([
self._fetchX2ManysBatched(list),
self._fetchReferencesBatched(list)
- );
+ ]);
});
defs.push(def);
}
}
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* batch request for x2ms for datapoint of type list
*
* @param {Object} list
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchX2ManysBatched: function (list) {
var defs = [];
@@ -2871,7 +2877,7 @@ var BasicModel = AbstractModel.extend({
defs.push(this._fetchX2ManyBatched(list, fieldNames[i]));
}
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* For a non-static list, batches requests for all its sublists' records.
@@ -2881,7 +2887,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} list a valid resource object, its data must be another
* list containing records
* @param {string} fieldName
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchX2ManySingleBatch: function (list, fieldName) {
var self = this;
@@ -2904,7 +2910,7 @@ var BasicModel = AbstractModel.extend({
* Called by _readGroup to make only one 'read' rpc by fieldName.
*
* @param {Object} list a valid resource object
- * @returns {Deferred}
+ * @returns {Promise}
*/
_fetchX2ManysSingleBatch: function (list) {
var defs = [];
@@ -2915,7 +2921,7 @@ var BasicModel = AbstractModel.extend({
defs.push(this._fetchX2ManySingleBatch(list, fieldNames[i]));
}
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Generates an object mapping field names to their changed value in a given
@@ -3584,12 +3590,12 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [options.keepEmptyGroups=false] if set, the groups not
* present in the read_group anymore (empty groups) will stay in the
* datapoint (used to mimic the kanban renderer behaviour for example)
- * @returns {Deferred}
+ * @returns {Promise}
*/
_load: function (dataPoint, options) {
if (options && options.onlyGroups &&
!(dataPoint.type === 'list' && dataPoint.groupedBy.length)) {
- return $.when(dataPoint);
+ return Promise.resolve(dataPoint);
}
if (dataPoint.type === 'record') {
@@ -3733,7 +3739,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} params.fields contains the description of each field
* @param {Object} params.context the context for the new record
* @param {string} params.viewType the key in fieldsInfo of the fields to load
- * @returns {Deferred<string>} resolves to the id for the created resource
+ * @returns {Promise<string>} resolves to the id for the created resource
*/
_makeDefaultRecord: function (modelName, params) {
var self = this;
@@ -3787,16 +3793,19 @@ var BasicModel = AbstractModel.extend({
return self.applyDefaultValues(record.id, result, {fieldNames: fieldNames})
.then(function () {
- var def = $.Deferred();
- self._performOnChange(record, fields_key).always(function () {
- if (record._warning) {
- if (params.allowWarning) {
- delete record._warning;
- } else {
- def.reject();
+ var def = new Promise(function (resolve, reject) {
+ var always = function () {
+ if (record._warning) {
+ if (params.allowWarning) {
+ delete record._warning;
+ } else {
+ reject();
+ }
}
- }
- def.resolve();
+ resolve();
+ };
+ self._performOnChange(record, fields_key)
+ .then(always).guardedCatch(always);
});
return def;
})
@@ -3865,13 +3874,13 @@ var BasicModel = AbstractModel.extend({
* @param {string[]} fields changed fields
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_performOnChange: function (record, fields, viewType) {
var self = this;
var onchangeSpec = this._buildOnchangeSpecs(record, viewType);
if (!onchangeSpec) {
- return $.when();
+ return Promise.resolve();
}
var idList = record.data.id ? [record.data.id] : [];
var options = {
@@ -3925,7 +3934,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {Object} [options.viewType] current viewType. If not set, we will
* assume main viewType from the record
- * @returns {Deferred<Object>} resolves to the finished resource
+ * @returns {Promise<Object>} resolves to the finished resource
*/
_postprocess: function (record, options) {
var self = this;
@@ -3954,7 +3963,7 @@ var BasicModel = AbstractModel.extend({
defs.push(this._fetchSpecialData(record, options));
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
return record;
});
},
@@ -3975,7 +3984,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string} [options.viewType] current viewType. If not set, we will
* assume main viewType from the record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_processX2ManyCommands: function (record, fieldName, commands, options) {
var self = this;
@@ -4076,10 +4085,10 @@ var BasicModel = AbstractModel.extend({
x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
- return $.when(
+ return Promise.all([
self._fetchX2ManysBatched(x2manyList),
self._fetchReferencesBatched(x2manyList)
- );
+ ]);
});
defs.push(def);
}
@@ -4090,7 +4099,7 @@ var BasicModel = AbstractModel.extend({
defs.push(self._fetchNameGets(x2manyList, name));
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Reads data from server for all missing fields.
@@ -4099,7 +4108,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} list a valid resource object
* @param {interger[]} resIDs
* @param {string[]} fieldNames to check and read if missing
- * @returns {Deferred<Object>}
+ * @returns {Promise<Object>}
*/
_readMissingFields: function (list, resIDs, fieldNames) {
var self = this;
@@ -4128,7 +4137,7 @@ var BasicModel = AbstractModel.extend({
context: list.getContext(),
});
} else {
- def = $.when(_.map(missingIDs, function (id) {
+ def = Promise.resolve(_.map(missingIDs, function (id) {
return {id:id};
}));
}
@@ -4174,7 +4183,7 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} list valid resource object
* @param {Object} [options] @see _load
- * @returns {Deferred<Object>} resolves to the fetched group object
+ * @returns {Promise<Object>} resolves to the fetched group object
*/
_readGroup: function (list, options) {
var self = this;
@@ -4285,20 +4294,20 @@ var BasicModel = AbstractModel.extend({
});
}
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function (groups) {
if (!options || !options.onlyGroups) {
// generate the res_ids of the main list, being the concatenation
// of the fetched res_ids in each group
- list.res_ids = _.flatten(_.map(arguments, function (group) {
+ list.res_ids = _.flatten(_.map(groups, function (group) {
return group ? group.res_ids : [];
}));
}
return list;
}).then(function () {
- var fetchingDefs = [];
- fetchingDefs.push(self._fetchX2ManysSingleBatch(list));
- fetchingDefs.push(self._fetchReferencesSingleBatch(list));
- return $.when.apply($, fetchingDefs).then(function () {
+ return Promise.all([
+ self._fetchX2ManysSingleBatch(list),
+ self._fetchReferencesSingleBatch(list)
+ ]).then(function () {
return list;
});
});
@@ -4309,11 +4318,11 @@ var BasicModel = AbstractModel.extend({
* instead of a /search_read.
*
* @param {Object} list a valid resource object
- * @returns {Deferred<Object>} resolves to the fetched list object
+ * @returns {Promise<Object>} resolves to the fetched list object
*/
_readUngroupedList: function (list) {
var self = this;
- var def = $.when();
+ var def = Promise.resolve();
// generate the current count and res_ids list by applying the changes
list = this._applyX2ManyOperations(list);
@@ -4375,7 +4384,7 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {boolean} [options.keepChanges=false] if true, doesn't discard the
* changes on the record before reloading it
- * @returns {Deferred<string>} resolves to the id of the resource
+ * @returns {Promise<string>} resolves to the id of the resource
*/
_reload: function (id, options) {
options = options || {};
@@ -4464,7 +4473,7 @@ var BasicModel = AbstractModel.extend({
* /search_read because the data may not be static (for ex, a list view).
*
* @param {Object} list
- * @returns {Deferred}
+ * @returns {Promise}
*/
_searchReadUngroupedList: function (list) {
var self = this;
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 27821081f96..2bc63f47073 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -61,13 +61,13 @@ var BasicRenderer = AbstractRenderer.extend({
* before notifying them).
*
* @param {string} recordID
- * @return {Deferred}
+ * @return {Promise}
*/
commitChanges: function (recordID) {
var defs = _.map(this.allFieldWidgets[recordID], function (widget) {
return widget.commitChanges();
});
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Updates the internal state of the renderer to the new state. By default,
@@ -86,7 +86,7 @@ var BasicRenderer = AbstractRenderer.extend({
* @param {string} id
* @param {string[]} fields
* @param {OdooEvent} ev
- * @returns {Deferred<AbstractField[]>} resolved with the list of widgets
+ * @returns {Promise<AbstractField[]>} resolved with the list of widgets
* that have been reset
*/
confirmChange: function (state, id, fields, ev) {
@@ -118,7 +118,7 @@ var BasicRenderer = AbstractRenderer.extend({
// state before evaluation
defs.push(this._updateAllModifiers(record));
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
return resetWidgets;
});
},
@@ -295,7 +295,7 @@ var BasicRenderer = AbstractRenderer.extend({
*
* @private
* @param {AbstractField} widget
- * @returns {boolean|Deferred<boolean>} @see AbstractField.isValid
+ * @returns {boolean|Promise<boolean>} @see AbstractField.isValid
*/
_canWidgetBeSaved: function (widget) {
var modifiers = this._getEvaluatedModifiers(widget.__node, widget.record);
@@ -386,6 +386,7 @@ var BasicRenderer = AbstractRenderer.extend({
* @param {Object} node the attrs coming from the arch
*/
_postProcessField: function (widget, node) {
+ this._handleAttributes(widget.$el, node);
},
/**
* Registers or updates the modifiers data associated to the given node.
@@ -503,7 +504,7 @@ var BasicRenderer = AbstractRenderer.extend({
* Render the view
*
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
var oldAllFieldWidgets = this.allFieldWidgets;
@@ -520,7 +521,7 @@ var BasicRenderer = AbstractRenderer.extend({
/**
* Instantiates the appropriate AbstractField specialization for the given
* node and prepares its rendering and addition to the DOM. Indeed, the
- * rendering of the widget will be started and the associated deferred will
+ * rendering of the widget will be started and the associated promise will
* be added to the 'defs' attribute. This is supposed to be created and
* deleted by the calling code if necessary.
*
@@ -559,28 +560,31 @@ var BasicRenderer = AbstractRenderer.extend({
widget.__node = node; // TODO get rid of this if possible one day
- // Prepare widget rendering and save the related deferred
+ // Prepare widget rendering and save the related promise
var def = widget._widgetRenderAndInsert(function () {});
- var async = def.state() === 'pending';
- var $el = async ? $('<div>') : widget.$el;
- if (async) {
- this.defs.push(def);
- }
+ var $el = $('<div>');
+
+ this.defs.push(def);
// Update the modifiers registration by associating the widget and by
// giving the modifiers options now (as the potential callback is
// associated to new widget)
var self = this;
def.then(function () {
- if (async) {
- $el.replaceWith(widget.$el);
- }
+ // when the caller of renderFieldWidget uses something like
+ // this.renderFieldWidget(...).addClass(...), the class is added on
+ // the temporary div and not on the actual element that will be
+ // rendered. As we do not return a promise and some callers cannot
+ // wait for this.defs, we copy those attributes to the final element.
+ widget.$el.attr($el.getAttributes());
+
+ $el.replaceWith(widget.$el);
self._registerModifiers(node, record, widget, {
callback: function (element, modifiers, record) {
element.$el.toggleClass('o_field_empty', !!(
- record.data.id
- && (modifiers.readonly || mode === 'readonly')
- && !element.widget.isSet()
+ record.data.id &&
+ (modifiers.readonly || mode === 'readonly') &&
+ !element.widget.isSet()
));
},
keepBaseMode: !!options.keepBaseMode,
@@ -618,10 +622,10 @@ var BasicRenderer = AbstractRenderer.extend({
* possible.
*
* @abstract
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderView: function () {
- return $.when();
+ return Promise.resolve();
},
/**
* Instantiate custom widgets
@@ -637,22 +641,17 @@ var BasicRenderer = AbstractRenderer.extend({
this.widgets.push(widget);
- // Prepare widget rendering and save the related deferred
+ // Prepare widget rendering and save the related promise
var def = widget._widgetRenderAndInsert(function () {});
- var async = def.state() === 'pending';
- if (async) {
- this.defs.push(def);
- }
- var $el = async ? $('<div>') : widget.$el;
+ this.defs.push(def);
+ var $el = $('<div>');
var self = this;
def.then(function () {
self._handleAttributes(widget.$el, node);
self._registerModifiers(node, record, widget);
widget.$el.addClass('o_widget');
- if (async) {
- $el.replaceWith(widget.$el);
- }
+ $el.replaceWith(widget.$el);
});
return $el;
@@ -709,7 +708,7 @@ var BasicRenderer = AbstractRenderer.extend({
*
* @private
* @param {Object} record
- * @returns {Deferred} resolved once finished
+ * @returns {Promise} resolved once finished
*/
_updateAllModifiers: function (record) {
var self = this;
@@ -722,7 +721,7 @@ var BasicRenderer = AbstractRenderer.extend({
});
delete this.defs;
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 313bed44708..008bbcf8775 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -93,7 +93,7 @@ var BasicView = AbstractView.extend({
*
* @override
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadData: function (model) {
if (this.recordID) {
@@ -195,7 +195,7 @@ var BasicView = AbstractView.extend({
}
});
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
return model.get(record.id);
});
}
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 0934ae462b3..fa41706de2b 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -143,7 +143,7 @@ var CalendarController = AbstractController.extend({
*
* @private
* @param {string} to either 'prev', 'next' or 'today'
- * @returns {Deferred}
+ * @returns {Promise}
*/
_move: function (to) {
this.model[to]();
@@ -153,7 +153,7 @@ var CalendarController = AbstractController.extend({
* @private
* @param {Object} record
* @param {integer} record.id
- * @returns {Deferred}
+ * @returns {Promise}
*/
_updateRecord: function (record) {
return this.model.updateRecord(record).then(this.reload.bind(this, {}));
@@ -388,18 +388,18 @@ var CalendarController = AbstractController.extend({
self.quick.destroy();
self.quick = null;
self.reload();
+ self.quickCreating = false;
})
- .fail(function (error, errorEvent) {
+ .guardedCatch(function (result) {
+ var errorEvent = result.event;
// This will occurs if there are some more fields required
// Preventdefaulting the error event will prevent the traceback window
errorEvent.preventDefault();
event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
- })
- .always(function () {
self.quickCreating = false;
- });
+ })
},
/**
* Called when we want to open or close the sidebar.
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index aaf52057ad6..9d8cf04f30f 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -177,7 +177,7 @@ return AbstractModel.extend({
/**
* @override
* @param {any} params
- * @returns {Deferred}
+ * @returns {Promise}
*/
load: function (params) {
var self = this;
@@ -199,15 +199,18 @@ return AbstractModel.extend({
// fields to display color, e.g.: user_id.partner_id
this.fieldColor = params.fieldColor;
- if (!this.preload_def) {
- this.preload_def = $.Deferred();
- $.when(
- this._rpc({model: this.modelName, method: 'check_access_rights', args: ["write", false]}),
- this._rpc({model: this.modelName, method: 'check_access_rights', args: ["create", false]}))
- .then(function (write, create) {
- self.write_right = write;
- self.create_right = create;
- self.preload_def.resolve();
+ if (!this.preloadPromise) {
+ this.preloadPromise = new Promise(function (resolve, reject) {
+ Promise.all([
+ self._rpc({model: self.modelName, method: 'check_access_rights', args: ["write", false]}),
+ self._rpc({model: self.modelName, method: 'check_access_rights', args: ["create", false]})
+ ]).then(function (result) {
+ var write = result[0];
+ var create = result[1];
+ self.write_right = write;
+ self.create_right = create;
+ resolve();
+ }).guardedCatch(reject);
});
}
@@ -229,7 +232,7 @@ return AbstractModel.extend({
}
});
- return this.preload_def.then(this._loadCalendar.bind(this));
+ return this.preloadPromise.then(this._loadCalendar.bind(this));
},
/**
* Move the current date range to the next period
@@ -247,7 +250,7 @@ return AbstractModel.extend({
* @override
* @param {Object} [params.context]
* @param {Array} [params.domain]
- * @returns {Deferred}
+ * @returns {Promise}
*/
reload: function (handle, params) {
if (params.domain) {
@@ -308,7 +311,7 @@ return AbstractModel.extend({
/**
* @param {Object} record
* @param {integer} record.id
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateRecord: function (record) {
// Cannot modify actual name yet
@@ -430,7 +433,7 @@ return AbstractModel.extend({
},
/**
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadCalendar: function () {
var self = this;
@@ -439,7 +442,7 @@ return AbstractModel.extend({
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
return self._rpc({
model: self.modelName,
method: 'search_read',
@@ -450,10 +453,10 @@ return AbstractModel.extend({
.then(function (events) {
self._parseServerData(events);
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
- return $.when(
+ return Promise.all([
self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data)
- );
+ ]);
});
});
},
@@ -461,7 +464,7 @@ return AbstractModel.extend({
* @private
* @param {any} element
* @param {any} events
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadColors: function (element, events) {
if (this.fieldColor) {
@@ -472,16 +475,16 @@ return AbstractModel.extend({
});
this.model_color = this.fields[fieldName].relation || element.model;
}
- return $.Deferred().resolve();
+ return Promise.resolve();
},
/**
* @private
* @param {any} filter
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadFilter: function (filter) {
if (!filter.write_model) {
- return $.when();
+ return Promise.resolve();
}
var field = this.fields[filter.fieldName];
@@ -540,7 +543,7 @@ return AbstractModel.extend({
* @private
* @param {any} element
* @param {any} events
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadRecordsToFilters: function (element, events) {
var self = this;
@@ -608,7 +611,7 @@ return AbstractModel.extend({
to_read[model] = _.object(res);
}));
});
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
_.each(self.data.filters, function (filter) {
if (filter.write_model) {
return;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 0ea7d60b1ce..c66b7bcc665 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -85,7 +85,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
});
defs.push(def);
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
@@ -193,7 +193,7 @@ return AbstractRenderer.extend({
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this._initSidebar();
@@ -460,7 +460,7 @@ return AbstractRenderer.extend({
*
* @override method from AbstractRenderer
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
var $calendar = this.$calendar;
@@ -504,7 +504,7 @@ return AbstractRenderer.extend({
this.$sidebar_container.toggleClass('o_sidebar_hidden', fullWidth);
this.$sidebar.toggleClass('o_hidden', fullWidth);
- this._renderFilters();
+ var filterProm = this._renderFilters();
this.$calendar.appendTo('body');
if (scrollTop) {
this.$calendar.fullCalendar('reinitView');
@@ -514,7 +514,7 @@ return AbstractRenderer.extend({
this._renderEvents();
this.$calendar.prependTo(this.$('.o_calendar_view'));
- return this._super.apply(this, arguments);
+ return Promise.all([filterProm, this._super.apply(this, arguments)]);
},
/**
* Render all events
@@ -529,25 +529,47 @@ return AbstractRenderer.extend({
* Render all filters
*
* @private
+ * @returns {Promise} resolved when all filters have been rendered
*/
_renderFilters: function () {
- var self = this;
_.each(this.filters || (this.filters = []), function (filter) {
filter.destroy();
});
if (this.state.fullWidth) {
return;
}
- _.each(this.state.filters, function (options) {
+ return this._renderFiltersOneByOne();
+ },
+ /**
+ * Renders each filter one by one, waiting for the first filter finished to
+ * be rendered and appended to render the next one.
+ * We need to do like this since render a filter is asynchronous, we don't
+ * know which one will be appened at first and we want tp force them to be
+ * rendered in order.
+ *
+ * @param {number} filterIndex if not set, 0 by default
+ * @returns {Promise} resolved when all filters have been rendered
+ */
+ _renderFiltersOneByOne: function (filterIndex) {
+ filterIndex = filterIndex || 0;
+ var arrFilters = _.toArray(this.state.filters);
+ var prom;
+ if (filterIndex < arrFilters.length) {
+ var options = arrFilters[filterIndex];
if (!_.find(options.filters, function (f) {return f.display == null || f.display;})) {
return;
}
- options.getColor = self.getColor.bind(self);
- options.fields = self.state.fields;
+
+ var self = this;
+ options.getColor = this.getColor.bind(this);
+ options.fields = this.state.fields;
var filter = new SidebarFilter(self, options);
- filter.appendTo(self.$sidebar);
- self.filters.push(filter);
- });
+ prom = filter.appendTo(this.$sidebar).then(function () {
+ return self._renderFiltersOneByOne(filterIndex + 1);
+ });
+ this.filters.push(filter);
+ }
+ return Promise.resolve(prom);
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_controller.js b/addons/web/static/src/js/views/control_panel/control_panel_controller.js
index 40b1c45b9c7..913c226ba85 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_controller.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_controller.js
@@ -63,14 +63,14 @@ var ControlPanelController = mvc.Controller.extend({
},
/**
* @param {Object} state a ControlPanelModel state
- * @returns {Deferred<Object>} the result of `getSearchState`
+ * @returns {Promise<Object>} the result of `getSearchState`
*/
importState: function (state) {
var defs = [];
this.model.importState(state);
defs.push(this.getSearchQuery());
defs.push(this.renderer.updateState(this.model.get()));
- return $.when(defs).then(function (defsResults) {
+ return Promise.all(defs).then(function (defsResults) {
return defsResults[0];
});
},
@@ -127,7 +127,7 @@ var ControlPanelController = mvc.Controller.extend({
},
/**
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_reportNewQueryAndRender: function () {
this.trigger_up('search', this.model.getQuery());
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js
index e59dbfb1036..ad0d80de7a3 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_model.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js
@@ -325,7 +325,7 @@ var ControlPanelModel = mvc.Model.extend({
* @param {string[]} [params.searchMenuTypes=[]]
* @param {Object} [params.timeRanges]
* @param {boolean} [params.withSearchBar]
- * @returns {Deferred}
+ * @returns {Promise}
*/
load: function (params) {
var self = this;
@@ -337,11 +337,11 @@ var ControlPanelModel = mvc.Model.extend({
// and the info comming from arch put in params.groups (if any)
// will be lost. This is not a problem because the state
// won't be use elsewhere.
- return $.when();
+ return Promise.resolve();
}
if (params.initialState) {
this.importState(params.initialState);
- return $.when();
+ return Promise.resolve();
} else {
var groups = params.groups || [];
groups.forEach(function (group) {
@@ -351,7 +351,7 @@ var ControlPanelModel = mvc.Model.extend({
this._createEmptyGroup('groupBy');
}
this._createGroupOfTimeRanges();
- return $.when.apply($, self._loadSearchDefaults()).then(function () {
+ return Promise.all(self._loadSearchDefaults()).then(function () {
return self._loadFavorites().then(function () {
if (self.query.length === 0) {
self._activateDefaultFilters();
@@ -910,7 +910,7 @@ var ControlPanelModel = mvc.Model.extend({
* if this.activateDefaultFavorite is true.
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadFavorites: function () {
var self = this;
@@ -980,7 +980,7 @@ var ControlPanelModel = mvc.Model.extend({
* is asynchronous.
*
* @private
- * @returns {Deferred[]}
+ * @returns {Promise[]}
*/
_loadSearchDefaults: function () {
var self = this;
@@ -1044,7 +1044,7 @@ var ControlPanelModel = mvc.Model.extend({
*
* @private
* @param {Object} favorite
- * @returns {Deferred}
+ * @returns {Promise}
*/
_saveQuery: function (favorite) {
var self = this;
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
index fd9037cdf6f..a1da2925eb6 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
@@ -86,7 +86,7 @@ var ControlPanelRenderer = Renderer.extend({
var superDef = this._super.apply(this, arguments);
var searchDef = this._renderSearch();
- return $.when(superDef, searchDef).then(function () {
+ return Promise.all([superDef, searchDef]).then(function () {
self._setSearchMenusVisibility();
});
},
@@ -148,7 +148,7 @@ var ControlPanelRenderer = Renderer.extend({
* Update the state of the renderer state. It retriggers a full rerendering.
*
* @param {Object} state
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateState: function (state) {
this.state = state;
@@ -261,7 +261,7 @@ var ControlPanelRenderer = Renderer.extend({
* Renderer the search bar and the search menus
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderSearch: function () {
var defs = [];
@@ -274,11 +274,11 @@ var ControlPanelRenderer = Renderer.extend({
if (this.withSearchBar) {
defs.push(this._renderSearchBar());
}
- return $.when(this, defs).then(this._focusSearchInput.bind(this));
+ return Promise.all(defs).then(this._focusSearchInput.bind(this));
},
/**
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderSearchBar: function () {
// TODO: might need a reload instead of a destroy/instantiate
@@ -315,7 +315,7 @@ var ControlPanelRenderer = Renderer.extend({
*
* @private
* @param {string} menuType
- * @returns {Deferred}
+ * @returns {Promise}
*/
_setupMenu: function (menuType) {
var Menu;
@@ -345,7 +345,7 @@ var ControlPanelRenderer = Renderer.extend({
* Instantiate the search menu determined by this.searchMenuTypes.
*
* @private
- * @returns {Deferred[]}
+ * @returns {Promise[]}
*/
_setupMenus: function () {
this.$subMenus = this._getSubMenusPlace();
diff --git a/addons/web/static/src/js/views/control_panel/search/filter_menu.js b/addons/web/static/src/js/views/control_panel/search/filter_menu.js
index 04f333b37c4..4ded35cfecc 100644
--- a/addons/web/static/src/js/views/control_panel/search/filter_menu.js
+++ b/addons/web/static/src/js/views/control_panel/search/filter_menu.js
@@ -67,14 +67,14 @@ var FilterMenu = DropdownMenu.extend({
* Add a proposition inside the custom filter edition menu.
*
* @private
- * @returns {$.Deferred}
+ * @returns {Promise}
*/
_appendProposition: function () {
// make modern sear_filters code!!! It works but...
var prop = new search_filters.ExtendedSearchProposition(this, this.fields);
this.propositions.push(prop);
this.$('.o_apply_filter').prop('disabled', false);
- prop.insertBefore(this.$addFilterMenu);
+ return prop.insertBefore(this.$addFilterMenu);
},
/**
* Confirm a filter proposition, creates it and add it to the menu.
diff --git a/addons/web/static/src/js/views/control_panel/search/search_bar.js b/addons/web/static/src/js/views/control_panel/search/search_bar.js
index 77d6d7fa5e9..4c2e71a378f 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_bar.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_bar.js
@@ -46,7 +46,7 @@ var SearchBar = Widget.extend({
defs.push(self._renderFacet(facet));
});
defs.push(this._setupAutoCompletion());
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
//--------------------------------------------------------------------------
@@ -116,18 +116,21 @@ var SearchBar = Widget.extend({
* @param {Function} callback
*/
_getAutoCompleteSources: function (req, callback) {
- var defs = _.invoke(this.autoCompleteSources, 'getAutocompletionValues', req.term);
- $.when.apply($, defs).then(function () {
- callback(_(arguments).chain()
+ var defs = this.autoCompleteSources.map(function (source) {
+ return source.getAutocompletionValues(req.term);
+ });
+ Promise.all(defs).then(function (result) {
+ var resultCleaned = _(result).chain()
.compact()
.flatten(true)
- .value());
- });
+ .value();
+ callback(resultCleaned);
+ });
},
/**
* @private
* @param {Object} facet
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderFacet: function (facet) {
var searchFacet = new SearchFacet(this, facet);
@@ -136,7 +139,7 @@ var SearchBar = Widget.extend({
},
/**
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_setupAutoCompletion: function () {
var self = this;
diff --git a/addons/web/static/src/js/views/control_panel/search/search_bar_autocomplete_sources.js b/addons/web/static/src/js/views/control_panel/search/search_bar_autocomplete_sources.js
index f1ac0ee0f2e..8adc385a789 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_bar_autocomplete_sources.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_bar_autocomplete_sources.js
@@ -32,7 +32,7 @@ var FilterInterface = Class.extend(mixins.EventDispatcherMixin, {
* label. They will be used by search_bar in @_onAutoCompleteSelected.
*
* @param {string} value value to getAutocompletionValues
- * @returns {Deferred<null|Array>}
+ * @returns {Promise<null|Array>}
*/
getAutocompletionValues: function (value) {
var result;
@@ -46,7 +46,7 @@ var FilterInterface = Class.extend(mixins.EventDispatcherMixin, {
},
}];
}
- return $.when(result);
+ return Promise.resolve(result);
},
});
@@ -83,7 +83,7 @@ var Field = FilterInterface.extend(ServicesMixin, {
* @override
*/
getAutocompletionValues: function (value) {
- return $.when([{
+ return Promise.resolve([{
label: this._getAutocompletionLabel(value),
facet: this._getFacetValue(value),
}]);
@@ -194,7 +194,7 @@ var CharField = Field.extend({
* @override
*/
getAutocompletionValues: function (value) {
- if (_.isEmpty(value)) { return $.when(null); }
+ if (_.isEmpty(value)) { return Promise.resolve(null); }
return this._super.apply(this, arguments);
},
});
@@ -210,7 +210,7 @@ var NumberField = Field.extend({
*/
getAutocompletionValues: function (value) {
var val = this.parse(value);
- if (isNaN(val)) { return $.when(); }
+ if (isNaN(val)) { return Promise.resolve(); }
return this._super.apply(this, arguments);
},
});
@@ -261,8 +261,8 @@ var SelectionField = Field.extend({
facet: self._getFacetValue(sel)
};
}).value();
- if (_.isEmpty(results)) { return $.when(null); }
- return $.when.call(null, [{
+ if (_.isEmpty(results)) { return Promise.resolve(null); }
+ return Promise.resolve([{
label: _.escape(this.attrs.string)
}].concat(results));
},
@@ -310,15 +310,15 @@ var DateField = Field.extend({
t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
v = field_utils.parse[t](value, {type: t}, {timezone: true});
} catch (e) {
- return $.when(null);
+ return Promise.resolve(null);
}
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
- if (!m.isValid()) { return $.when(null); }
+ if (!m.isValid()) { return Promise.resolve(null); }
var dateString = field_utils.format[t](m, {type: t});
var label = this._getAutocompletionLabel(dateString);
var facet = this._getFacetValue(dateString, m.toDate());
- return $.when([{
+ return Promise.resolve([{
label: label,
facet: facet,
}]);
@@ -379,9 +379,9 @@ var ManyToOneField = CharField.extend({
* @override
*/
getAutocompletionValues: function (value) {
- if (_.isEmpty(value)) { return $.when(null); }
+ if (_.isEmpty(value)) { return Promise.resolve(null); }
var label = this._getAutocompletionLabel(value);
- return $.when([{
+ return Promise.resolve([{
label: label,
facet: this._getFacetValue(value),
expand: this._expand.bind(this),
diff --git a/addons/web/static/src/js/views/control_panel/search/search_filters.js b/addons/web/static/src/js/views/control_panel/search/search_filters.js
index 80a9019c02d..ae0ed12772e 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_filters.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_filters.js
@@ -40,7 +40,9 @@ var ExtendedSearchProposition = Widget.extend({
this.value = null;
},
start: function () {
- return this._super().done(this.proxy('changed'));
+ var parent = this._super();
+ parent.then(this.proxy('changed'));
+ return parent;
},
changed: function () {
var nval = this.$(".o_searchview_extended_prop_field").val();
@@ -234,10 +236,10 @@ var DateTime = Field.extend({
return str;
},
start: function () {
- return $.when(
+ return Promise.all([
this._super.apply(this, arguments),
this._create_new_widget("datewidget_0")
- );
+ ]);
},
_create_new_widget: function (name) {
this[name] = new (this._get_widget_class())(this);
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index bc61e92a89a..99aabc6e66d 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -41,7 +41,7 @@ var FieldManagerMixin = {
* @param {string} dataPointID
* @param {Object} changes
* @param {OdooEvent} event
- * @returns {Deferred} resolves when the change has been done, and the UI
+ * @returns {Promise} resolves when the change has been done, and the UI
* updated
*/
_applyChanges: function (dataPointID, changes, event) {
@@ -66,10 +66,10 @@ var FieldManagerMixin = {
* @param {string} id basicModel Id for the changed record
* @param {string[]} fields the fields (names) that have been changed
* @param {OdooEvent} event the event that triggered the change
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmChange: function (id, fields, event) {
- return $.when();
+ return Promise.resolve();
},
/**
* This method will be called whenever a save has been triggered by a change
@@ -79,10 +79,10 @@ var FieldManagerMixin = {
* @see _onFieldChanged
* @abstract
* @param {string} id The basicModel ID for the saved record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmSave: function (id) {
- return $.when();
+ return Promise.resolve();
},
//--------------------------------------------------------------------------
@@ -103,9 +103,9 @@ var FieldManagerMixin = {
// subrecord's form view), otherwise it bubbles up to the main form view
// but its model doesn't have any data related to the given dataPointID
event.stopPropagation();
- this._applyChanges(event.data.dataPointID, event.data.changes, event)
- .done(event.data.onSuccess || function () {})
- .fail(event.data.onFailure || function () {});
+ return this._applyChanges(event.data.dataPointID, event.data.changes, event)
+ .then(event.data.onSuccess || function () {})
+ .guardedCatch(event.data.onFailure || function () {});
},
/**
* Some widgets need to trigger a reload of their data. For example, a
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index d0400ef041f..5616a5099bc 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -84,7 +84,7 @@ var FormController = BasicController.extend({
* @todo make record creation a basic controller feature
* @param {string} [parentID] if given, the parentID will be used as parent
* for the new record.
- * @returns {Deferred}
+ * @returns {Promise}
*/
createRecord: function (parentID) {
var self = this;
@@ -165,20 +165,23 @@ var FormController = BasicController.extend({
* @override method from BasicController
* @param {jQueryElement} $node
* @param {Object} options
+ * @returns {Promise}
*/
renderPager: function ($node, options) {
options = _.extend({}, options, {
validate: this.canBeDiscarded.bind(this),
});
- this._super($node, options);
+ return this._super($node, options);
},
/**
* Instantiate and render the sidebar if a sidebar is requested
* Sets this.sidebar
* @param {jQuery} [$node] a jQuery node where the sidebar should be
* inserted
+ * @return {Promise}
**/
renderSidebar: function ($node) {
+ var self = this;
if (this.hasSidebar) {
var otherItems = [];
if (this.is_action_enabled('delete')) {
@@ -203,11 +206,12 @@ var FormController = BasicController.extend({
},
actions: _.extend(this.toolbarActions, {other: otherItems}),
});
- this.sidebar.appendTo($node);
-
- // Show or hide the sidebar according to the view mode
- this._updateSidebar();
+ return this.sidebar.appendTo($node).then(function() {
+ // Show or hide the sidebar according to the view mode
+ self._updateSidebar();
+ });
}
+ return Promise.resolve();
},
/**
* Show a warning message if the user modified a translated field. For each
@@ -296,7 +300,7 @@ var FormController = BasicController.extend({
* @private
* @override method from field manager mixin
* @param {string} id - id of the previously changed record
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmSave: function (id) {
if (id === this.handle) {
@@ -393,14 +397,19 @@ var FormController = BasicController.extend({
* @override
* @private
* @param {Object} state
- * @returns {Deferred}
+ * @returns {Promise}
*/
_update: function () {
- var title = this.getTitle();
- this._setTitle(title);
- this._updateButtons();
- this._updateSidebar();
- return this._super.apply(this, arguments).then(this.autofocus.bind(this));
+ var self = this;
+
+ return this._super.apply(this, arguments).then(function() {
+ var title = self.getTitle();
+ self._setTitle(title);
+ self._updateButtons();
+ self._updateSidebar();
+
+ self.autofocus();
+ });
},
/**
* @private
@@ -470,13 +479,11 @@ var FormController = BasicController.extend({
}
var attrs = ev.data.attrs;
if (attrs.confirm) {
- var d = $.Deferred();
- Dialog.confirm(this, attrs.confirm, {
- confirm_callback: saveAndExecuteAction,
- }).on("closed", null, function () {
- d.resolve();
+ def = new Promise(function (resolve, reject) {
+ Dialog.confirm(this, attrs.confirm, {
+ confirm_callback: saveAndExecuteAction,
+ }).on("closed", null, resolve);
});
- def = d.promise();
} else if (attrs.special === 'cancel') {
def = this._callButtonAction(attrs, ev.data.record);
} else if (!attrs.special || attrs.special === 'save') {
@@ -484,7 +491,7 @@ var FormController = BasicController.extend({
def = saveAndExecuteAction();
}
- def.always(this._enableButtons.bind(this));
+ def.then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
},
/**
* Called when the user wants to create a new record -> @see createRecord
@@ -659,9 +666,7 @@ var FormController = BasicController.extend({
ev.stopPropagation(); // Prevent x2m lines to be auto-saved
var self = this;
this._disableButtons();
- this.saveRecord().always(function () {
- self._enableButtons();
- });
+ this.saveRecord().then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
},
/**
* Called when user swipes left. Move to next record.
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index ca26d7e90ad..97ab575cbc3 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -251,7 +251,7 @@ var FormRenderer = BasicRenderer.extend({
* @param {string} [params.mode] new mode, either 'edit' or 'readonly'
* @param {string[]} [params.fieldNames] if given, the renderer will only
* update the fields in this list
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateState: function (state, params) {
this.mode = (params && 'mode' in params) ? params.mode : this.mode;
@@ -342,8 +342,8 @@ var FormRenderer = BasicRenderer.extend({
* @private
*/
_postProcessField: function (widget, node) {
+ this._super.apply(this, arguments);
this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
- this._handleAttributes(widget.$el, node);
if (JSON.parse(node.attrs.default_focus || "0")) {
this.defaultFocusField = widget;
}
@@ -355,7 +355,14 @@ var FormRenderer = BasicRenderer.extend({
*/
_renderButtonBox: function (node) {
var self = this;
- var $result = $('<' + node.tag + '>', { 'class': 'o_not_full' });
+ var $result = $('<' + node.tag + '>', {class: 'o_not_full'});
+
+ // The rendering of buttons may be async (see renderFieldWidget), so we
+ // must wait for the buttons to be ready (and their modifiers to be
+ // applied) before manipulating them, as we check if they are visible or
+ // not. To do so, we extract from this.defs the promises corresponding
+ // to the buttonbox buttons, and wait for them to be resolved.
+ var nextDefIndex = this.defs.length;
var buttons = _.map(node.children, function (child) {
if (child.tag === 'button') {
return self._renderStatButton(child);
@@ -363,48 +370,62 @@ var FormRenderer = BasicRenderer.extend({
return self._renderNode(child);
}
});
- var buttons_partition = _.partition(buttons, function ($button) {
- return $button.is('.o_invisible_modifier');
+
+ // At this point, each button is an empty div that will be replaced by
+ // the real $el of the button when it is ready (with replaceWith).
+ // However, this only works if the empty div is appended somewhere, so
+ // we here append them into a wrapper, and unwrap them once they have
+ // been replaced.
+ var $tempWrapper = $('<div>');
+ _.each(buttons, function ($button) {
+ $button.appendTo($tempWrapper);
});
- var invisible_buttons = buttons_partition[0];
- var visible_buttons = buttons_partition[1];
+ var defs = this.defs.slice(nextDefIndex);
+ Promise.all(defs).then(function () {
+ buttons = $tempWrapper.children();
+ var buttons_partition = _.partition(buttons, function (button) {
+ return $(button).is('.o_invisible_modifier');
+ });
+ var invisible_buttons = buttons_partition[0];
+ var visible_buttons = buttons_partition[1];
- // Get the unfolded buttons according to window size
- var nb_buttons = [2, 2, 4, 6][config.device.size_class] || 7;
- var unfolded_buttons = visible_buttons.slice(0, nb_buttons).concat(invisible_buttons);
+ // Get the unfolded buttons according to window size
+ var nb_buttons = [2, 2, 4, 6][config.device.size_class] || 7;
+ var unfolded_buttons = visible_buttons.slice(0, nb_buttons).concat(invisible_buttons);
- // Get the folded buttons
- var folded_buttons = visible_buttons.slice(nb_buttons);
- if (folded_buttons.length === 1) {
- unfolded_buttons = buttons;
- folded_buttons = [];
- }
+ // Get the folded buttons
+ var folded_buttons = visible_buttons.slice(nb_buttons);
+ if (folded_buttons.length === 1) {
+ unfolded_buttons = buttons;
+ folded_buttons = [];
+ }
- // Toggle class to tell if the button box is full (CSS requirement)
- var full = (visible_buttons.length > nb_buttons);
- $result.toggleClass('o_full', full).toggleClass('o_not_full', !full);
+ // Toggle class to tell if the button box is full (CSS requirement)
+ var full = (visible_buttons.length > nb_buttons);
+ $result.toggleClass('o_full', full).toggleClass('o_not_full', !full);
- // Add the unfolded buttons
- _.each(unfolded_buttons, function ($button) {
- $button.appendTo($result);
- });
+ // Add the unfolded buttons
+ _.each(unfolded_buttons, function (button) {
+ $(button).appendTo($result);
+ });
- // Add the dropdown with folded buttons if any
- if (folded_buttons.length) {
- $result.append(dom.renderButton({
- attrs: {
- class: 'oe_stat_button o_button_more dropdown-toggle',
- 'data-toggle': 'dropdown',
- },
- text: _t("More"),
- }));
+ // Add the dropdown with folded buttons if any
+ if (folded_buttons.length) {
+ $result.append(dom.renderButton({
+ attrs: {
+ 'class': 'oe_stat_button o_button_more dropdown-toggle',
+ 'data-toggle': 'dropdown',
+ },
+ text: _t("More"),
+ }));
- var $dropdown = $("<div>", {'class': "dropdown-menu o_dropdown_more", role: "menu"});
- _.each(folded_buttons, function ($button) {
- $button.addClass('dropdown-item').appendTo($dropdown);
- });
- $dropdown.appendTo($result);
- }
+ var $dropdown = $("<div>", {class: "dropdown-menu o_dropdown_more", role: "menu"});
+ _.each(folded_buttons, function (button) {
+ $(button).addClass('dropdown-item').appendTo($dropdown);
+ });
+ $dropdown.appendTo($result);
+ }
+ });
this._handleAttributes($result, node);
this._registerModifiers(node, this.state, $result);
@@ -477,6 +498,7 @@ var FormRenderer = BasicRenderer.extend({
_renderInnerGroup: function (node) {
var self = this;
var $result = $('<table/>', {class: 'o_group o_inner_group'});
+ var $tbody = $('<tbody />').appendTo($result);
this._handleAttributes($result, node);
this._registerModifiers(node, this.state, $result);
@@ -490,7 +512,7 @@ var FormRenderer = BasicRenderer.extend({
var rows = [];
var $currentRow = $('<tr/>');
var currentColspan = 0;
- _.each(node.children, function (child) {
+ node.children.forEach(function (child) {
if (child.tag === 'newline') {
rows.push($currentRow);
$currentRow = $('<tr/>');
@@ -537,7 +559,7 @@ var FormRenderer = BasicRenderer.extend({
var $el = $(el);
$el.css('width', ((parseInt($el.attr('colspan'), 10) || 1) * nonLabelColSize) + '%');
});
- $result.append($tr);
+ $tbody.append($tr);
});
return $result;
@@ -575,7 +597,7 @@ var FormRenderer = BasicRenderer.extend({
*
* For fields, it will return the $el of the field widget. Note that this
* method is synchronous, field widgets are instantiated and appended, but
- * if they are asynchronous, they register their deferred in this.defs, and
+ * if they are asynchronous, they register their promises in this.defs, and
* the _renderView method will properly wait.
*
* @private
@@ -705,7 +727,8 @@ var FormRenderer = BasicRenderer.extend({
if (node.attrs.class) {
$result.addClass(node.attrs.class);
}
- $result.append(_.map(node.children, this._renderNode.bind(this)));
+ var allNodes = node.children.map(this._renderNode.bind(this));
+ $result.append(allNodes);
return $result;
},
/**
@@ -870,7 +893,7 @@ var FormRenderer = BasicRenderer.extend({
_renderTagSheet: function (node) {
this.has_sheet = true;
var $sheet = $('<div>', {class: 'clearfix o_form_sheet'});
- $sheet.append(_.map(node.children, this._renderNode.bind(this)));
+ $sheet.append(node.children.map(this._renderNode.bind(this)));
return $sheet;
},
/**
@@ -885,12 +908,12 @@ var FormRenderer = BasicRenderer.extend({
},
/**
* Main entry point for the rendering. From here, we call _renderNode on
- * the root of the arch, then, when every deferred (from the field widgets)
+ * the root of the arch, then, when every promise (from the field widgets)
* are done, it will resolves itself.
*
* @private
* @override method from BasicRenderer
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderView: function () {
var self = this;
@@ -901,17 +924,17 @@ var FormRenderer = BasicRenderer.extend({
var $form = this._renderNode(this.arch).addClass(this.className);
delete this.defs;
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
self._updateView($form.contents());
if (self.state.res_id in self.alertFields) {
self.displayTranslationAlert();
}
- }, function () {
- $form.remove();
}).then(function(){
if (self.lastActivatedFieldIndex >= 0) {
self._activateNextFieldWidget(self.state, self.lastActivatedFieldIndex);
}
+ }).guardedCatch(function () {
+ $form.remove();
});
},
/**
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 5a00350c68f..0703d9713d5 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -93,7 +93,7 @@ var FormView = BasicView.extend({
*
* @private
* @param {Widget} parent the parent of the model, if it has to be created
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadSubviews: function (parent) {
var self = this;
@@ -145,7 +145,7 @@ var FormView = BasicView.extend({
}
});
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* We set here the limit for the number of records fetched (in one page).
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 4c359a4778f..91b017524d8 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -50,7 +50,7 @@ return AbstractModel.extend({
* @param {string[]} params.domain
* @param {Object} params.intervalMapping object linking fieldNames with intervals.
* this could be useful to simplify the code. For now this parameter is not used.
- * @returns {Deferred} The deferred does not return a handle, we don't need
+ * @returns {Promise} The promise does not return a handle, we don't need
* to keep track of various entities.
*/
load: function (params) {
@@ -92,7 +92,7 @@ return AbstractModel.extend({
* @param {string[]} [params.groupBy]
* @param {string} [params.mode] one of 'bar', 'pie', 'line'
* @param {string} [params.measure] a valid field name
- * @returns {Deferred}
+ * @returns {Promise}
*/
reload: function (handle, params) {
if ('context' in params) {
@@ -130,7 +130,7 @@ return AbstractModel.extend({
}
if ('mode' in params) {
this.chart.mode = params.mode;
- return $.when();
+ return Promise.resolve();
}
return this._loadGraph();
},
@@ -145,7 +145,7 @@ return AbstractModel.extend({
* in the field list, because they can be defined with an aggregation
* function, such as my_date:week
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadGraph: function () {
var groupedBy = this.chart.groupedBy;
@@ -186,7 +186,7 @@ return AbstractModel.extend({
}).then(this._processData.bind(this, 'comparisonData')));
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Since read_group is insane and returns its result on different keys
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 9353301ca09..6311df5d8b2 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -99,7 +99,7 @@ return AbstractRenderer.extend({
*
* @override
* @private
- * @returns {Deferred} The _super deferred is actually resolved immediately
+ * @returns {Promise} The _super promise is actually resolved immediately
*/
_render: function () {
if (this.to_remove) {
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index 2828ca491ca..b4aecd7a236 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -97,10 +97,7 @@ var KanbanColumn = Widget.extend({
this.$header = this.$('.o_kanban_header');
for (var i = 0; i < this.data_records.length; i++) {
- var def = this._addRecord(this.data_records[i]);
- if (def.state() === 'pending') {
- defs.push(def);
- }
+ defs.push(this._addRecord(this.data_records[i]));
}
if (!config.device.isMobile) {
@@ -170,7 +167,7 @@ var KanbanColumn = Widget.extend({
this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget: this}));
}
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* Called when a record has been quick created, as a new column is rendered
@@ -193,7 +190,7 @@ var KanbanColumn = Widget.extend({
/**
* Adds the quick create record to the top of the column.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
addQuickCreate: function () {
if (this.folded) {
@@ -205,7 +202,7 @@ var KanbanColumn = Widget.extend({
}
if (this.quickCreateWidget) {
- return $.Deferred().reject();
+ return Promise.reject();
}
this.trigger_up('close_quick_create'); // close other quick create widgets
this.trigger_up('start_quick_create');
@@ -245,7 +242,7 @@ var KanbanColumn = Widget.extend({
* @param {Object} [options]
* @param {string} [options.position]
* 'before' to add at the top, add at the bottom by default
- * @return {Deferred}
+ * @return {Promise}
*/
_addRecord: function (recordState, options) {
var record = new this.KanbanRecord(this, recordState, this.record_options);
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 3b41b26e015..58174c7812d 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -76,6 +76,7 @@ var KanbanController = BasicController.extend({
/**
* @param {jQueryElement} $node
+ * @returns {Promise}
*/
renderButtons: function ($node) {
if (this.hasButtons && this.is_action_enabled('create')) {
@@ -86,8 +87,9 @@ var KanbanController = BasicController.extend({
this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this));
this._updateButtons();
- this.$buttons.appendTo($node);
+ return Promise.resolve(this.$buttons.appendTo($node));
}
+ return Promise.resolve();
},
/**
* Override to add the domain coming from the searchPanel (if any) to the
@@ -110,7 +112,7 @@ var KanbanController = BasicController.extend({
params.domain = this.controlPanelDomain.concat(this.searchPanelDomain);
var superProm = this._super.apply(this, arguments);
var searchPanelProm = this._updateSearchPanel();
- return $.when(superProm, searchPanelProm).then(function () {
+ return Promise.all([superProm, searchPanelProm]).then(function () {
// searchPanel has been re-rendered, so re-render the view
return self.renderer.render();
});
@@ -124,7 +126,7 @@ var KanbanController = BasicController.extend({
* @override method comes from field manager mixin
* @private
* @param {string} id local id from the basic record data
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmSave: function (id) {
var data = this.model.get(this.handle, {raw: true});
@@ -197,7 +199,7 @@ var KanbanController = BasicController.extend({
/**
* @param {number[]} ids
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_resequenceColumns: function (ids) {
var state = this.model.get(this.handle, {raw: true});
@@ -212,7 +214,7 @@ var KanbanController = BasicController.extend({
* @private
* @param {string} column_id
* @param {string[]} ids
- * @returns {Deferred}
+ * @returns {Promise}
*/
_resequenceRecords: function (column_id, ids) {
var self = this;
@@ -246,7 +248,7 @@ var KanbanController = BasicController.extend({
},
/**
* @private
- * @returns {$.Promise}
+ * @returns {Promise}
*/
_updateSearchPanel: function () {
return this._searchPanel.update({searchDomain: this.controlPanelDomain});
@@ -296,7 +298,7 @@ var KanbanController = BasicController.extend({
self.renderer.updateColumn(db_id, data);
});
});
- }).fail(this.reload.bind(this, {}));
+ }).guardedCatch(this.reload.bind(this));
},
/**
* @private
@@ -400,7 +402,7 @@ var KanbanController = BasicController.extend({
var relatedModelName = state.fields[state.groupedBy[0]].relation;
this.model
.deleteRecords([column.db_id], relatedModelName)
- .done(function () {
+ .then(function () {
self.update({}, {reload: !column.isEmpty()});
});
},
@@ -415,11 +417,12 @@ var KanbanController = BasicController.extend({
var self = this;
this.model.loadColumnRecords(ev.data.columnID).then(function (dbID) {
var data = self.model.get(dbID);
- self.renderer.updateColumn(dbID, data);
- self._updateEnv();
- if (ev.data.onSuccess) {
- ev.data.onSuccess();
- }
+ return self.renderer.updateColumn(dbID, data).then(function() {
+ self._updateEnv();
+ if (ev.data.onSuccess) {
+ ev.data.onSuccess();
+ }
+ });
});
},
/**
@@ -470,8 +473,8 @@ var KanbanController = BasicController.extend({
this.model.createRecordInGroup(column.db_id, values)
.then(update)
- .fail(function (error, ev) {
- ev.preventDefault();
+ .guardedCatch(function (reason) {
+ reason.event.preventDefault();
var columnState = self.model.get(column.db_id, {raw: true});
var context = columnState.getContext();
var state = self.model.get(self.handle, {raw: true});
@@ -538,11 +541,15 @@ var KanbanController = BasicController.extend({
*
* @private
* @param {OdooEvent} ev
+ * @param {function} [ev.data.onSuccess] callback to execute after applying
+ * changes
*/
_onUpdateRecord: function (ev) {
+ var onSuccess = ev.data.onSuccess;
+ delete ev.data.onSuccess;
var changes = _.clone(ev.data);
ev.data.force_save = true;
- this._applyChanges(ev.target.db_id, changes, ev);
+ this._applyChanges(ev.target.db_id, changes, ev).then(onSuccess);
},
/**
* Allow the user to archive/restore all the records of a column.
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index bfb6f5e0cc9..8b1135d3f15 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -20,7 +20,7 @@ var KanbanModel = BasicModel.extend({
*
* @param {string} groupID localID of the group
* @param {integer} resId id of the record
- * @returns {Deferred<string>} resolves to the local id of the new record
+ * @returns {Promise<string>} resolves to the local id of the new record
*/
addRecordToGroup: function (groupID, resId) {
var self = this;
@@ -52,7 +52,7 @@ var KanbanModel = BasicModel.extend({
*
* @param {string} name
* @param {string} parentID localID of the parent of the group
- * @returns {Deferred<string>} resolves to the local id of the new group
+ * @returns {Promise<string>} resolves to the local id of the new group
*/
createGroup: function (name, parentID) {
var self = this;
@@ -60,7 +60,7 @@ var KanbanModel = BasicModel.extend({
var groupBy = parent.groupedBy[0];
var groupByField = parent.fields[groupBy];
if (!groupByField || groupByField.type !== 'many2one') {
- return $.Deferred().reject(); // only supported when grouped on m2o
+ return Promise.reject(); // only supported when grouped on m2o
}
return this._rpc({
model: groupByField.relation,
@@ -99,7 +99,7 @@ var KanbanModel = BasicModel.extend({
*
* @param {string} groupID
* @param {Object} values
- * @returns {Deferred} resolved with the local id of the created record
+ * @returns {Promise} resolved with the local id of the created record
*/
createRecordInGroup: function (groupID, values) {
var self = this;
@@ -187,7 +187,7 @@ var KanbanModel = BasicModel.extend({
* Opens a given group and loads its <limit> first records
*
* @param {string} groupID
- * @returns {Deferred}
+ * @returns {Promise}
*/
loadColumnRecords: function (groupID) {
var dataPoint = this.localData[groupID];
@@ -198,7 +198,7 @@ var KanbanModel = BasicModel.extend({
* Load more records in a group.
*
* @param {string} groupID localID of the group
- * @returns {Deferred<string>} resolves to the localID of the group
+ * @returns {Promise<string>} resolves to the localID of the group
*/
loadMore: function (groupID) {
var group = this.localData[groupID];
@@ -213,7 +213,7 @@ var KanbanModel = BasicModel.extend({
* @param {string} recordID localID of the record
* @param {string} groupID localID of the new group of the record
* @param {string} parentID localID of the parent
- * @returns {Deferred<string[]>} resolves to a pair [oldGroupID, newGroupID]
+ * @returns {Promise<string[]>} resolves to a pair [oldGroupID, newGroupID]
*/
moveRecord: function (recordID, groupID, parentID) {
var self = this;
@@ -330,7 +330,7 @@ var KanbanModel = BasicModel.extend({
/**
* @private
* @param {Object} dataPoint
- * @returns {Deferred<Object>}
+ * @returns {Promise<Object>}
*/
_readProgressBarGroup: function (list, options) {
var self = this;
@@ -345,7 +345,8 @@ var KanbanModel = BasicModel.extend({
context: list.context,
},
});
- return $.when(groupsDef, progressBarDef).then(function (a, data) {
+ return Promise.all([groupsDef, progressBarDef]).then(function (results) {
+ var data = results[1];
_.each(list.data, function (groupID) {
var group = self.localData[groupID];
group.progressBarValues = _.extend({
@@ -365,13 +366,13 @@ var KanbanModel = BasicModel.extend({
*
* @private
* @param {Object} list a list of groups
- * @returns {Deferred}
+ * @returns {Promise}
*/
_readTooltipFields: function (list) {
var self = this;
var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
- return $.when();
+ return Promise.resolve();
}
var groupIds = _.reduce(list.data, function (groupIds, id) {
var res_id = self.get(id, {raw: true}).res_id;
@@ -400,16 +401,16 @@ var KanbanModel = BasicModel.extend({
});
});
}
- return $.when();
+ return Promise.resolve();
},
/**
- * Reloads all progressbar data. This is done after given deferred and
- * insures that the given deferred's result is not lost.
+ * Reloads all progressbar data. This is done after given promise and
+ * insures that the given promise's result is not lost.
*
* @private
* @param {string} recordID
- * @param {Deferred} def
- * @returns {Deferred}
+ * @param {Promise} def
+ * @returns {Promise}
*/
_reloadProgressBarGroupFromRecord: function (recordID, def) {
var element = this.localData[recordID];
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 3dc1d6788bf..116de142b0c 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -62,7 +62,7 @@ var KanbanRecord = Widget.extend({
* @override
*/
start: function () {
- return $.when(this._super.apply(this, arguments), this._render());
+ return Promise.all([this._super.apply(this, arguments), this._render()]);
},
/**
* Called each time the record is attached to the DOM.
@@ -85,7 +85,7 @@ var KanbanRecord = Widget.extend({
* Re-renders the record with a new state
*
* @param {Object} state
- * @returns {Deferred}
+ * @returns {Promise}
*/
update: function (state) {
// detach the widgets because the record will empty its $el, which will
@@ -147,7 +147,7 @@ var KanbanRecord = Widget.extend({
}
if (typeof variable === 'string') {
var index = 0;
- for (var i = 0 ; i < variable.length ; i++) {
+ for (var i = 0; i < variable.length; i++) {
index += variable.charCodeAt(i);
}
return index % NB_KANBAN_RECORD_COLORS;
@@ -195,7 +195,7 @@ var KanbanRecord = Widget.extend({
field = options.preview_image;
var unique = this.record.__last_update && this.record.__last_update.value.replace(/[^0-9]/g, '');
var session = this.getSession();
- url = session.url('/web/image', {model: model, field: field, id: id, unique: unique});
+ url = session.url('/web/image', { model: model, field: field, id: id, unique: unique });
if (cache !== undefined) {
// Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10);
@@ -300,6 +300,7 @@ var KanbanRecord = Widget.extend({
// field's widgets point of view
// that dict being shared between records, we don't modify it
// in place
+ var self = this;
var attrs = Object.create(null);
_.each(this.fieldsInfo[field_name], function (value, key) {
if (_.str.startsWith(key, 't-att-')) {
@@ -308,13 +309,13 @@ var KanbanRecord = Widget.extend({
}
attrs[key] = value;
});
- var options = _.extend({}, this.options, {attrs: attrs});
+ var options = _.extend({}, this.options, { attrs: attrs });
var widget = new Widget(this, field_name, this.state, options);
var def = widget.replace($field);
- if (def.state() === 'pending') {
- this.defs.push(def);
- }
- this._setFieldDisplay(widget.$el, field_name);
+ this.defs.push(def);
+ def.then(function () {
+ self._setFieldDisplay(widget.$el, field_name);
+ });
return widget;
},
_processWidgets: function () {
@@ -324,23 +325,23 @@ var KanbanRecord = Widget.extend({
var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state);
- var def = widget._widgetRenderAndInsert(function () {});
- if (def.state() === 'pending') {
- self.defs.push(def);
- }
- widget.$el.addClass('o_widget');
- $field.replaceWith(widget.$el);
+ var def = widget._widgetRenderAndInsert(function () { });
+ self.defs.push(def);
+ def.then(function () {
+ $field.replaceWith(widget.$el);
+ widget.$el.addClass('o_widget');
+ });
});
},
/**
* Renders the record
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
this.defs = [];
this._replaceElement(this.qweb.render('kanban-box', this.qweb_context));
- this.$el.addClass('o_kanban_record').attr("tabindex",0);
+ this.$el.addClass('o_kanban_record').attr("tabindex", 0);
this.$el.attr('role', 'article');
this.$el.data('record', this);
if (this.$el.hasClass('oe_kanban_global_click') ||
@@ -357,9 +358,9 @@ var KanbanRecord = Widget.extend({
this._attachTooltip();
// We use boostrap tooltips for better and faster display
- this.$('span.o_tag').tooltip({delay: {'show': 50}});
+ this.$('span.o_tag').tooltip({ delay: { 'show': 50 } });
- return $.when.apply(this, this.defs);
+ return Promise.all(this.defs);
},
/**
* Sets particular classnames on a field $el according to the
@@ -419,7 +420,7 @@ var KanbanRecord = Widget.extend({
var colorHelp = _.str.sprintf(_t("Card color: %s"), this._getColorname(this.recordData[color_field]));
var colorClass = this._getColorClassname(this.recordData[color_field]);
this.$el.addClass(colorClass);
- this.$el.prepend('<span title="' + colorHelp + '" aria-label="' + colorHelp +'" role="img" class="oe_kanban_color_help"/>');
+ this.$el.prepend('<span title="' + colorHelp + '" aria-label="' + colorHelp + '" role="img" class="oe_kanban_color_help"/>');
}
},
/**
@@ -454,7 +455,7 @@ var KanbanRecord = Widget.extend({
r.raw_value = value.toDate();
} else if (r.type === 'one2many' || r.type === 'many2many') {
r.raw_value = value.count ? value.res_ids : [];
- } else if (r.type === 'many2one' ) {
+ } else if (r.type === 'many2one') {
r.raw_value = value && value.res_id || false;
} else {
r.raw_value = value;
@@ -554,13 +555,13 @@ var KanbanRecord = Widget.extend({
switch (type) {
case 'edit':
- this.trigger_up('open_record', {id: this.db_id, mode: 'edit'});
+ this.trigger_up('open_record', { id: this.db_id, mode: 'edit' });
break;
case 'open':
- this.trigger_up('open_record', {id: this.db_id});
+ this.trigger_up('open_record', { id: this.db_id });
break;
case 'delete':
- this.trigger_up('kanban_record_delete', {id: this.db_id, record: this});
+ this.trigger_up('kanban_record_delete', { id: this.db_id, record: this });
break;
case 'action':
case 'object':
diff --git a/addons/web/static/src/js/views/kanban/kanban_record_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_record_quick_create.js
index 741cc6a898a..e69914acd49 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record_quick_create.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record_quick_create.js
@@ -47,14 +47,14 @@ var RecordQuickCreate = Widget.extend({
*/
willStart: function () {
var self = this;
- var def1 = this._super.apply(this, arguments);
- var def2;
+ var superWillStart = this._super.apply(this, arguments);
+ var viewsLoaded;
if (this.formViewRef) {
var views = [[false, 'form']];
var context = _.extend({}, this.context, {
form_view_ref: this.formViewRef,
});
- def2 = this.loadViews(this.model, context, views);
+ viewsLoaded = this.loadViews(this.model, context, views);
} else {
var fieldsView = {};
fieldsView.arch = '<form>' +
@@ -65,9 +65,9 @@ var RecordQuickCreate = Widget.extend({
};
fieldsView.fields = fields;
fieldsView.viewFields = fields;
- def2 = $.when({form: fieldsView});
+ viewsLoaded = Promise.resolve({form: fieldsView});
}
- def2 = def2.then(function (fieldsViews) {
+ viewsLoaded = viewsLoaded.then(function (fieldsViews) {
var formView = new QuickCreateFormView(fieldsViews.form, {
context: self.context,
modelName: self.model,
@@ -78,7 +78,7 @@ var RecordQuickCreate = Widget.extend({
return self.controller.appendTo(document.createDocumentFragment());
});
});
- return $.when(def1, def2);
+ return Promise.all([superWillStart, viewsLoaded]);
},
/**
* @override
@@ -113,7 +113,7 @@ var RecordQuickCreate = Widget.extend({
* have been made yet
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
cancel: function () {
var self = this;
@@ -129,7 +129,6 @@ var RecordQuickCreate = Widget.extend({
//--------------------------------------------------------------------------
/**
- * @override
* @private
* @param {Object} [options]
* @param {boolean} [options.openRecord] set to true to directly open the
@@ -139,7 +138,7 @@ var RecordQuickCreate = Widget.extend({
var self = this;
if (this._disabled) {
// don't do anything if we are already creating a record
- return $.Deferred();
+ return;
}
// disable the widget to prevent the user from creating multiple records
// with the current values ; if the create works, the widget will be
@@ -157,13 +156,13 @@ var RecordQuickCreate = Widget.extend({
} else {
self._enableQuickCreate();
}
- }).fail(this._enableQuickCreate.bind(this));
+ }).guardedCatch(this._enableQuickCreate.bind(this));
},
/**
* Notifies the environment that the quick creation must be cancelled
*
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_cancel: function () {
this.trigger_up('cancel_quick_create');
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 252b4587af6..eda15f706d3 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -148,7 +148,7 @@ var KanbanRenderer = BasicRenderer.extend({
/**
* Displays the quick create record in the first column.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
addQuickCreate: function () {
return this.widgets[0].addQuickCreate();
@@ -187,7 +187,7 @@ var KanbanRenderer = BasicRenderer.extend({
* @param {boolean} [options.openQuickCreate] if true, directly opens the
* QuickCreate widget in the updated column
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateColumn: function (localID, columnState, options) {
var self = this;
@@ -204,7 +204,7 @@ var KanbanRenderer = BasicRenderer.extend({
if (options && options.openQuickCreate) {
def = newColumn.addQuickCreate();
}
- return $.when(def).then(function () {
+ return Promise.resolve(def).then(function () {
newColumn.$el.insertAfter(column.$el);
self._toggleNoContentHelper();
// When a record has been quick created, the new column directly
@@ -221,7 +221,7 @@ var KanbanRenderer = BasicRenderer.extend({
* Updates a given record with its new state.
*
* @param {Object} recordState
- * @returns {Deferred}
+ * @returns {Promise}
*/
updateRecord: function (recordState) {
var isGrouped = !!this.state.groupedBy.length;
@@ -242,7 +242,7 @@ var KanbanRenderer = BasicRenderer.extend({
if (record) {
return record.update(recordState);
}
- return $.when();
+ return Promise.resolve();
},
/**
* @override
@@ -295,10 +295,13 @@ var KanbanRenderer = BasicRenderer.extend({
* @param {integer} nbDivs the number of divs to append
*/
_renderGhostDivs: function (fragment, nbDivs) {
+ var ghostDefs = [];
for (var $ghost, i = 0; i < nbDivs; i++) {
$ghost = $('<div>').addClass('o_kanban_record o_kanban_ghost');
- $ghost.appendTo(fragment);
+ var def = $ghost.appendTo(fragment);
+ ghostDefs.push(def);
}
+ return Promise.all(ghostDefs);
},
/**
* Renders an grouped kanban view in a fragment.
@@ -321,10 +324,7 @@ var KanbanRenderer = BasicRenderer.extend({
def = column.appendTo(fragment);
self.widgets.push(column);
}
- if (def.state() === 'pending') {
- self.defs.push(def);
- }
-
+ self.defs.push(def);
});
// remove previous sorting
@@ -359,13 +359,13 @@ var KanbanRenderer = BasicRenderer.extend({
this.quickCreate = new ColumnQuickCreate(this, {
examples: this.examples,
});
- this.quickCreate.appendTo(fragment).then(function () {
+ this.defs.push(this.quickCreate.appendTo(fragment).then(function () {
// Open it directly if there is no column yet
if (!self.state.data.length) {
self.quickCreate.toggleFold();
self._renderExampleBackground(fragment);
}
- });
+ }));
}
}
},
@@ -382,13 +382,14 @@ var KanbanRenderer = BasicRenderer.extend({
var kanbanRecord = new KanbanRecord(self, record, self.recordOptions);
self.widgets.push(kanbanRecord);
var def = kanbanRecord.appendTo(fragment);
- if (def.state() === 'pending') {
- self.defs.push(def);
- }
+ self.defs.push(def);
});
// append ghost divs to ensure that all kanban records are left aligned
- this._renderGhostDivs(fragment, 6);
+ var prom = Promise.all(self.defs).then(function () {
+ return self._renderGhostDivs(fragment, 6);
+ });
+ this.defs.push(prom);
},
/**
* @override
@@ -398,28 +399,31 @@ var KanbanRenderer = BasicRenderer.extend({
var self = this;
var oldWidgets = this.widgets;
this.widgets = [];
- this.$el.empty();
+ // render the kanban view
var isGrouped = !!this.state.groupedBy.length;
- this.$el.toggleClass('o_kanban_grouped', isGrouped);
- this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
var fragment = document.createDocumentFragment();
- // render the kanban view
- this.defs = [];
+ var defs = [];
+ this.defs = defs;
if (isGrouped) {
this._renderGrouped(fragment);
} else {
this._renderUngrouped(fragment);
}
- this.$el.append(fragment);
- this._toggleNoContentHelper();
- var defs = this.defs;
+ delete this.defs;
+
return this._super.apply(this, arguments).then(function () {
- _.invoke(oldWidgets, 'destroy');
- if (self._isInDom) {
- _.invoke(self.widgets, 'on_attach_callback');
- }
- return $.when.apply(null, defs);
+ return Promise.all(defs).then(function () {
+ self.$el.empty();
+ self.$el.toggleClass('o_kanban_grouped', isGrouped);
+ self.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
+ self.$el.append(fragment);
+ self._toggleNoContentHelper();
+ _.invoke(oldWidgets, 'destroy');
+ if (self._isInDom) {
+ _.invoke(self.widgets, 'on_attach_callback');
+ }
+ });
});
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
index a9576e9836f..eae16968833 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
@@ -75,7 +75,7 @@ KanbanRenderer.include({
/**
* Displays the quick create record in the active column
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
addQuickCreate: function () {
return this.widgets[this.activeColumnIndex].addQuickCreate();
@@ -260,26 +260,26 @@ KanbanRenderer.include({
* @private
* @param {integer} moveToIndex index of the column to move to
* @param {boolean} [animate=false] set to true to animate
- * @returns {Deferred} resolved when the new current group has been loaded
+ * @returns {Promise} resolved when the new current group has been loaded
* and displayed
*/
_moveToGroup: function (moveToIndex, animate) {
var self = this;
if (moveToIndex < 0 || moveToIndex >= this.widgets.length) {
this._layoutUpdate(animate);
- return $.when();
+ return Promise.resolve();
}
- var def = $.Deferred();
this.activeColumnIndex = moveToIndex;
var column = this.widgets[this.activeColumnIndex];
- this.trigger_up('kanban_load_records', {
- columnID: column.db_id,
- onSuccess: function () {
- self._layoutUpdate(animate);
- def.resolve();
- },
+ return new Promise(function (resolve) {
+ self.trigger_up('kanban_load_records', {
+ columnID: column.db_id,
+ onSuccess: function () {
+ self._layoutUpdate(animate);
+ resolve();
+ },
+ });
});
- return def;
},
/**
@@ -287,22 +287,23 @@ KanbanRenderer.include({
* @private
*/
_renderGrouped: function (fragment) {
- var result = this._super.apply(this, arguments);
- var data = [];
- _.each(this.state.data, function (group) {
- if (!group.value) {
- group = _.extend({}, group, {value: _t('Undefined')});
- data.unshift(group);
- }
- else {
- data.push(group);
- }
- });
+ var self = this;
+ this._super.apply(this, arguments);
+ this.defs.push(Promise.all(this.defs).then(function () {
+ var data = [];
+ _.each(self.state.data, function (group) {
+ if (!group.value) {
+ group = _.extend({}, group, {value: _t('Undefined')});
+ data.unshift(group);
+ } else {
+ data.push(group);
+ }
+ });
- $(qweb.render('KanbanView.MobileTabs', {
- data: data,
- })).prependTo(fragment);
- return result;
+ $(qweb.render('KanbanView.MobileTabs', {
+ data: data,
+ })).prependTo(fragment);
+ }));
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 37f7fdd42ef..49ba9b74034 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -139,7 +139,7 @@ var KanbanView = BasicView.extend({
/**
* @private
* @param {Widget} parent
- * @returns {$.Promise} resolved when the searchPanel is ready
+ * @returns {Promise} resolved when the searchPanel is ready
*/
_createSearchPanel: function (parent) {
var self = this;
diff --git a/addons/web/static/src/js/views/kanban/quick_create_form_view.js b/addons/web/static/src/js/views/kanban/quick_create_form_view.js
index af769d62095..8725c3104de 100644
--- a/addons/web/static/src/js/views/kanban/quick_create_form_view.js
+++ b/addons/web/static/src/js/views/kanban/quick_create_form_view.js
@@ -71,7 +71,7 @@ var QuickCreateFormController = FormController.extend({
*/
commitChanges: function () {
var mutexDef = this.mutex.getUnlockedDef();
- return $.when(mutexDef, this.renderer.commitChanges(this.handle));
+ return Promise.all([mutexDef, this.renderer.commitChanges(this.handle)]);
},
/**
* @returns {Object} the changes done on the current record
diff --git a/addons/web/static/src/js/views/kanban/search_panel.js b/addons/web/static/src/js/views/kanban/search_panel.js
index b906d02b89a..d06eacbdb07 100644
--- a/addons/web/static/src/js/views/kanban/search_panel.js
+++ b/addons/web/static/src/js/views/kanban/search_panel.js
@@ -55,7 +55,7 @@ var SearchPanel = Widget.extend({
var loadProm = this._fetchCategories().then(function () {
return self._fetchFilters().then(self._applyDefaultFilterValues.bind(self));
});
- return $.when(loadProm, this._super.apply(this, arguments));
+ return Promise.all([loadProm, this._super.apply(this, arguments)]);
},
/**
* @override
@@ -82,18 +82,18 @@ var SearchPanel = Widget.extend({
*
* @param {Object} params
* @param {Array[]} params.searchDomain domain coming from controlPanel
- * @returns {$.Promise}
+ * @returns {Promise}
*/
update: function (params) {
var currentSearchDomainStr = JSON.stringify(this.searchDomain);
var newSearchDomainStr = JSON.stringify(params.searchDomain);
- var def;
+ var filtersProm;
if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) {
this.needReload = false;
this.searchDomain = params.searchDomain;
- def = this._fetchFilters();
+ filtersProm = this._fetchFilters();
}
- return $.when(def).then(this._render.bind(this));
+ return Promise.resolve(filtersProm).then(this._render.bind(this));
},
//--------------------------------------------------------------------------
@@ -231,21 +231,21 @@ var SearchPanel = Widget.extend({
* Fetch values for each category. This is done only once, at startup.
*
* @private
- * @returns {$.Promise} resolved when all categories have been fetched
+ * @returns {Promise} resolved when all categories have been fetched
*/
_fetchCategories: function () {
var self = this;
- var defs = Object.keys(this.categories).map(function (categoryId) {
+ var proms = Object.keys(this.categories).map(function (categoryId) {
var category = self.categories[categoryId];
var field = self.fields[category.fieldName];
- var def;
+ var categoriesProm;
if (field.type === 'selection') {
var values = field.selection.map(function (value) {
return {id: value[0], display_name: value[1]};
});
- def = $.when(values);
+ categoriesProm = Promise.resolve(values);
} else {
- def = self._rpc({
+ categoriesProm = self._rpc({
method: 'search_panel_select_range',
model: self.model,
args: [category.fieldName],
@@ -254,18 +254,18 @@ var SearchPanel = Widget.extend({
return result.values;
});
}
- return def.then(function (values) {
+ return categoriesProm.then(function (values) {
self._createCategoryTree(categoryId, values);
});
});
- return $.when.apply($, defs);
+ return Promise.all(proms);
},
/**
* Fetch values for each filter. This is done at startup, and at each reload
* (when the controlPanel or searchPanel domain changes).
*
* @private
- * @returns {$.Promise} resolved when all filters have been fetched
+ * @returns {Promise} resolved when all filters have been fetched
*/
_fetchFilters: function () {
var self = this;
@@ -276,7 +276,7 @@ var SearchPanel = Widget.extend({
});
var categoryDomain = this._getCategoryDomain();
var filterDomain = this._getFilterDomain();
- var defs = Object.keys(this.filters).map(function (filterId) {
+ var proms = Object.keys(this.filters).map(function (filterId) {
var filter = self.filters[filterId];
return self._rpc({
method: 'search_panel_select_multi_range',
@@ -294,7 +294,7 @@ var SearchPanel = Widget.extend({
self._createFilterTree(filterId, values);
});
});
- return $.when.apply($, defs);
+ return Promise.all(proms);
},
/**
* Compute and return the domain based on the current active categories.
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index dfa7d272a49..055cd8177e3 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -65,7 +65,7 @@ var ListController = BasicController.extend({
* this method should be private, most of the code in the sidebar should be
* moved to the controller, and we should not use the getParent method...
*
- * @returns {Deferred<array[]>} a deferred that resolve to the active domain
+ * @returns {Promise<array[]>} a promise that resolve to the active domain
*/
getActiveDomain: function () {
// TODO: this method should be synchronous...
@@ -73,9 +73,9 @@ var ListController = BasicController.extend({
if (this.$('thead .o_list_record_selector input').prop('checked')) {
var searchQuery = this._controlPanel ? this._controlPanel.getSearchQuery() : {};
var record = self.model.get(self.handle, {raw: true});
- return $.when(record.getDomain().concat(searchQuery.domain || []));
+ return Promise.all(record.getDomain().concat(searchQuery.domain || []));
} else {
- return $.Deferred().resolve();
+ return Promise.resolve();
}
},
/*
@@ -143,6 +143,7 @@ var ListController = BasicController.extend({
* main buttons)
*
* @param {jQuery Node} $node
+ * @returns {Promise}
*/
renderSidebar: function ($node) {
var self = this;
@@ -180,10 +181,11 @@ var ListController = BasicController.extend({
},
actions: _.extend(this.toolbarActions, {other: other}),
});
- this.sidebar.appendTo($node);
-
- this._toggleSidebar();
+ return this.sidebar.appendTo($node).then(function() {
+ self._toggleSidebar();
+ });
}
+ return Promise.resolve();
},
/**
* Overrides to update the list of selected records
@@ -243,10 +245,11 @@ var ListController = BasicController.extend({
});
}).then(function (recordID) {
var state = self.model.get(self.handle);
- self.renderer.updateState(state, {});
- self.renderer.editRecord(recordID);
- self._updatePager();
- }).always(this._enableButtons.bind(this));
+ self.renderer.updateState(state, {})
+ .then(function () {
+ self.renderer.editRecord(recordID);
+ }).then(self._updatePager.bind(self));
+ }).then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
},
/**
* Archive the current selection
@@ -254,11 +257,11 @@ var ListController = BasicController.extend({
* @private
* @param {string[]} ids
* @param {boolean} archive
- * @returns {Deferred}
+ * @returns {Promise}
*/
_archive: function (ids, archive) {
if (ids.length === 0) {
- return $.when();
+ return Promise.resolve();
}
return this.model
.toggleActive(ids, !archive, this.handle)
@@ -298,7 +301,7 @@ var ListController = BasicController.extend({
* @override
* @param {string} id a basicmodel valid resource handle. It is supposed to
* be a record from the list view.
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmSave: function (id) {
var state = this.model.get(this.handle);
@@ -313,13 +316,13 @@ var ListController = BasicController.extend({
* @override
* @private
* @param {string} [recordID] - default to main recordID
- * @returns {Deferred}
+ * @returns {Promise}
*/
_discardChanges: function (recordID) {
if ((recordID || this.handle) === this.handle) {
recordID = this.renderer.getEditableRecordID();
if (recordID === null) {
- return $.when();
+ return Promise.resolve();
}
}
var self = this;
@@ -343,7 +346,7 @@ var ListController = BasicController.extend({
* @private
* @param {string} mode
* @param {string} [recordID] - default to main recordID
- * @returns {Deferred}
+ * @returns {Promise}
*/
_setMode: function (mode, recordID) {
if ((recordID || this.handle) !== this.handle) {
@@ -373,11 +376,13 @@ var ListController = BasicController.extend({
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
_update: function () {
- this._toggleSidebar();
- return this._super.apply(this, arguments);
+ var self = this;
+ return this._super.apply(this, arguments).then(function() {
+ self._toggleSidebar();
+ });
},
/**
* This helper simply makes sure that the control panel buttons matches the
@@ -471,7 +476,7 @@ var ListController = BasicController.extend({
var record = self.model.get(self.handle);
var editedRecord = record.data[ev.data.index];
self._setMode('edit', editedRecord.id)
- .done(ev.data.onSuccess);
+ .then(ev.data.onSuccess);
},
});
},
@@ -523,8 +528,8 @@ var ListController = BasicController.extend({
_onSaveLine: function (ev) {
var recordID = ev.data.recordID;
this.saveRecord(recordID)
- .done(ev.data.onSuccess)
- .fail(ev.data.onFailure);
+ .then(ev.data.onSuccess)
+ .guardedCatch(ev.data.onFailure);
},
/**
* When the current selection changes (by clicking on the checkboxes on the
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index ab5e5036dfa..4619cb86e66 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -93,7 +93,7 @@ ListRenderer.include({
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
// deliberately use the 'editable' attribute instead of '_isEditable'
@@ -171,7 +171,7 @@ ListRenderer.include({
* @param {string} id
* @param {string[]} fields
* @param {OdooEvent} ev
- * @returns {Deferred<AbstractField[]>} resolved with the list of widgets
+ * @returns {Promise<AbstractField[]>} resolved with the list of widgets
* that have been reset
*/
confirmUpdate: function (state, id, fields, ev) {
@@ -180,7 +180,7 @@ ListRenderer.include({
// store the cursor position to restore it once potential onchanges have
// been applied
var currentRowID, currentWidget, focusedElement, selectionRange;
- if (self.currentRow !== null) {
+ if (this.currentRow !== null) {
currentRowID = this.state.data[this.currentRow].id;
currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex];
if (currentWidget) {
@@ -212,10 +212,14 @@ ListRenderer.include({
});
var newRowIndex = _.findIndex(state.data, {id: id});
var $lastRow = $row;
- _.each(state.data, function (record, index) {
+ var defs = [];
+ self.defs = defs;
+ state.data.forEach(function (record, index) {
if (index === newRowIndex) {
return;
}
+ // FIXME: as we are manipulating the DOM directly, it will
+ // flicker if there is an async widget in the row
var $newRow = self._renderRow(record);
if (index < newRowIndex) {
$newRow.insertBefore($row);
@@ -224,20 +228,23 @@ ListRenderer.include({
$lastRow = $newRow;
}
});
- if (self.currentRow !== null) {
- self.currentRow = newRowIndex;
- return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true}).then(function () {
- // restore the cursor position
- currentRowID = self.state.data[newRowIndex].id;
- currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
- if (currentWidget) {
- focusedElement = currentWidget.getFocusableElement().get(0);
- if (selectionRange) {
- dom.setSelectionRange(focusedElement, selectionRange);
- }
+ delete self.defs;
+ return Promise.all(defs).then(function () {
+ if (self.currentRow !== null) {
+ self.currentRow = newRowIndex;
+ return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true});
+ }
+ }).then(function () {
+ // restore the cursor position
+ currentRowID = self.state.data[newRowIndex].id;
+ currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
+ if (currentWidget) {
+ focusedElement = currentWidget.getFocusableElement().get(0);
+ if (selectionRange) {
+ dom.setSelectionRange(focusedElement, selectionRange);
}
- });
- }
+ }
+ });
});
},
/**
@@ -294,7 +301,7 @@ ListRenderer.include({
*
* @param {string} recordID
* @param {string} mode
- * @returns {Deferred}
+ * @returns {Promise}
*/
setRowMode: function (recordID, mode) {
var self = this;
@@ -321,7 +328,7 @@ ListRenderer.include({
}
if (rowIndex < 0) {
- return $.when();
+ return Promise.resolve();
}
var editMode = (mode === 'edit');
@@ -350,7 +357,7 @@ ListRenderer.include({
options.mode = editMode ? 'edit' : 'readonly';
// Switch each cell to the new mode; note: the '_renderBodyCell'
- // function might fill the 'this.defs' variables with multiple deferred
+ // function might fill the 'this.defs' variables with multiple promise
// so we create the array and delete it after the rendering.
var defs = [];
this.defs = defs;
@@ -385,7 +392,7 @@ ListRenderer.include({
$row.toggleClass('o_selected_row', editMode);
$row.find('.o_list_record_selector input').prop('disabled', !record.res_id);
- return $.when.apply($, defs).then(function () {
+ return Promise.all(defs).then(function () {
// necessary to trigger resize on fieldtexts
core.bus.trigger('DOM_updated');
});
@@ -399,27 +406,32 @@ ListRenderer.include({
* prevent subsequent editions. These edits would be lost, because the list
* view only saves records when unselecting a row.
*
- * @returns {Deferred} The deferred resolves if the row was unselected (and
+ * @returns {Promise} The promise resolves if the row was unselected (and
* possibly removed). If may be rejected, when the row is dirty and the
* user refuses to discard its changes.
*/
unselectRow: function () {
+ var self = this;
// Protect against calling this method when no row is selected
if (this.currentRow === null) {
- return $.when();
+ return Promise.resolve();
}
var record = this.state.data[this.currentRow];
var recordWidgets = this.allFieldWidgets[record.id];
toggleWidgets(true);
- var def = $.Deferred();
- this.trigger_up('save_line', {
- recordID: record.id,
- onSuccess: def.resolve.bind(def),
- onFailure: def.reject.bind(def),
+ var prom = new Promise(function (resolve, reject) {
+ self.trigger_up('save_line', {
+ recordID: record.id,
+ onSuccess: resolve,
+ onFailure: reject,
+ });
});
- return def.fail(toggleWidgets.bind(null, false));
+ prom.guardedCatch(function() {
+ toggleWidgets(false);
+ });
+ return prom;
function toggleWidgets(disabled) {
_.each(recordWidgets, function (widget) {
@@ -511,7 +523,7 @@ ListRenderer.include({
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
this.currentRow = null;
@@ -524,10 +536,9 @@ ListRenderer.include({
* widget.
*
* @override
- * @returns {jQueryElement}
*/
_renderBody: function () {
- var $body = this._super();
+ var $body = this._super.apply(this, arguments);
if (this.hasHandle) {
$body.sortable({
axis: 'y',
@@ -568,15 +579,13 @@ ListRenderer.include({
* on the first real column.
*
* @override
- * @returns {jQueryElement}
+ * @returns {jQueryElement[]}
*/
_renderRows: function () {
- var self = this;
var $rows = this._super();
-
if (this.addCreateLine) {
var $tr = $('<tr>');
- var colspan = self._getNumberOfCols();
+ var colspan = this._getNumberOfCols();
if (this.handleField) {
colspan = colspan - 1;
@@ -589,7 +598,7 @@ ListRenderer.include({
$tr.append($td);
$rows.push($tr);
- _.each(self.creates, function (create, index) {
+ _.each(this.creates, function (create, index) {
var $a = $('<a href="#" role="button">')
.attr('data-context', create.context)
.text(create.string);
@@ -604,7 +613,7 @@ ListRenderer.include({
/**
* @override
* @private
- * @returns {Deferred} this deferred is resolved immediately
+ * @returns {Promise} this promise is resolved immediately
*/
_renderView: function () {
var self = this;
@@ -692,13 +701,13 @@ ListRenderer.include({
* @param {boolean} [options.force=false] if true, force selecting the cell
* even if seems to be already the selected one (useful after a re-
* rendering, to reset the focus on the correct field)
- * @return {Deferred} fails if no cell could be selected
+ * @return {Promise} fails if no cell could be selected
*/
_selectCell: function (rowIndex, fieldIndex, options) {
options = options || {};
// Do nothing if the user tries to select current cell
if (!options.force && rowIndex === this.currentRow && fieldIndex === this.currentFieldIndex) {
- return $.when();
+ return Promise.resolve();
}
var wrap = options.wrap === undefined ? true : options.wrap;
@@ -707,7 +716,7 @@ ListRenderer.include({
return this._selectRow(rowIndex).then(function () {
var record = self.state.data[rowIndex];
if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) {
- return $.Deferred().reject();
+ return Promise.reject();
}
// _activateFieldWidget might trigger an onchange,
// which requires currentFieldIndex to be set
@@ -721,7 +730,7 @@ ListRenderer.include({
});
if (fieldIndex < 0) {
self.currentFieldIndex = oldFieldIndex;
- return $.Deferred().reject();
+ return Promise.reject();
}
self.currentFieldIndex = fieldIndex;
});
@@ -730,12 +739,12 @@ ListRenderer.include({
* Activates the row at the given row index.
*
* @param {integer} rowIndex
- * @returns {Deferred}
+ * @returns {Promise}
*/
_selectRow: function (rowIndex) {
// Do nothing if already selected
if (rowIndex === this.currentRow) {
- return $.when();
+ return Promise.resolve();
}
// To select a row, the currently selected one must be unselected first
@@ -745,15 +754,15 @@ ListRenderer.include({
// The row to selected doesn't exist anymore (probably because
// an onchange triggered when unselecting the previous one
// removes rows)
- return $.Deferred().reject();
+ return Promise.reject();
}
// Notify the controller we want to make a record editable
- var def = $.Deferred();
- self.trigger_up('edit_line', {
- index: rowIndex,
- onSuccess: def.resolve.bind(def),
+ return new Promise(function (resolve) {
+ self.trigger_up('edit_line', {
+ index: rowIndex,
+ onSuccess: resolve,
+ });
});
- return def;
});
},
@@ -778,7 +787,7 @@ ListRenderer.include({
// but we do want to unselect current row
var self = this;
this.unselectRow().then(function () {
- self.trigger_up('add_record', {context: ev.currentTarget.dataset.context && [ev.currentTarget.dataset.context]}); // TODO write a test, the deferred was not considered
+ self.trigger_up('add_record', {context: ev.currentTarget.dataset.context && [ev.currentTarget.dataset.context]}); // TODO write a test, the promise was not considered
});
},
/**
@@ -864,12 +873,13 @@ ListRenderer.include({
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
+ var self = this;
ev.stopPropagation(); // stop the event, the action is done by this renderer
switch (ev.data.direction) {
case 'previous':
if (this.currentFieldIndex > 0) {
this._selectCell(this.currentRow, this.currentFieldIndex - 1, {wrap: false})
- .fail(this._moveToPreviousLine.bind(this));
+ .guardedCatch(this._moveToPreviousLine.bind(this));
} else {
this._moveToPreviousLine();
}
@@ -882,7 +892,7 @@ ListRenderer.include({
if (column.attrs.name === lastWidget.name) {
if (this.currentRow + 1 < this.state.data.length) {
this._selectCell(this.currentRow+1, 0, {wrap:false})
- .fail(this._moveToNextLine.bind(this));
+ .guardedCatch(this._moveToNextLine.bind(this));
} else {
var currentRowData = this.state.data[this.currentRow];
if (currentRowData.isDirty(currentRowData.id)) {
@@ -895,7 +905,7 @@ ListRenderer.include({
} else {
if (this.currentFieldIndex + 1 < this.columns.length) {
this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
- .fail(this._moveToNextLine.bind(this));
+ .guardedCatch(this._moveToNextLine.bind(this));
} else {
this._moveToNextLine();
}
@@ -911,8 +921,10 @@ ListRenderer.include({
ev.data.originalEvent.stopPropagation();
this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID,
+ onSuccess: function () {
+ self.$('.o_field_x2many_list_row_add a:first').focus();
+ }
});
- this.$('.o_field_x2many_list_row_add a:first').focus();
break;
}
},
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index ee081aa3c5b..760ab9ed7a8 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -250,9 +250,10 @@ var ListRenderer = BasicRenderer.extend({
* @returns {jQueryElement} a jquery element <tbody>
*/
_renderBody: function () {
+ var self = this;
var $rows = this._renderRows();
while ($rows.length < 4) {
- $rows.push(this._renderEmptyRow());
+ $rows.push(self._renderEmptyRow());
}
return $('<tbody>').append($rows);
},
@@ -306,7 +307,6 @@ var ListRenderer = BasicRenderer.extend({
}
if (node.attrs.widget || (options && options.renderWidgets)) {
var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
- this._handleAttributes($el, node);
return $td.append($el);
}
var name = node.attrs.name;
@@ -395,7 +395,7 @@ var ListRenderer = BasicRenderer.extend({
*
* @private
* @param {Object} group
- * @returns {JQueryElement} the pager's $el
+ * @returns {jQueryElement} the pager's $el
*/
_renderGroupPager: function (group) {
var pager = new Pager(this, group.count, group.offset + 1, group.limit);
@@ -416,9 +416,14 @@ var ListRenderer = BasicRenderer.extend({
// register the pager so that it can be destroyed on next rendering
this.pagers.push(pager);
- var fragment = document.createDocumentFragment();
- pager.appendTo(fragment); // starts the pager
- return pager.$el;
+ var pagerProm = pager._widgetRenderAndInsert(function () {}); // start the pager
+ this.defs.push(pagerProm);
+ var $el = $('<div>');
+ pagerProm.then(function () {
+ $el.replaceWith(pager.$el);
+ });
+
+ return $el;
},
/**
* Render the row that represent a group
@@ -514,10 +519,9 @@ var ListRenderer = BasicRenderer.extend({
* with the name of each fields
*
* @private
- * @param {boolean} isGrouped
* @returns {jQueryElement} a <thead> element
*/
- _renderHeader: function (isGrouped) {
+ _renderHeader: function () {
var $tr = $('<tr>')
.append(_.map(this.columns, this._renderHeaderCell.bind(this)));
if (this.hasSelectors) {
@@ -585,11 +589,9 @@ var ListRenderer = BasicRenderer.extend({
*/
_renderRow: function (record) {
var self = this;
- this.defs = []; // TODO maybe wait for those somewhere ?
- var $cells = _.map(this.columns, function (node, index) {
+ var $cells = this.columns.map(function (node, index) {
return self._renderBodyCell(record, node, index, { mode: 'readonly' });
});
- delete this.defs;
var $tr = $('<tr/>', { class: 'o_data_row' })
.data('id', record.id)
@@ -608,7 +610,7 @@ var ListRenderer = BasicRenderer.extend({
* @returns {jQueryElement[]} a list of <tr>
*/
_renderRows: function () {
- return _.map(this.state.data, this._renderRow.bind(this));
+ return this.state.data.map(this._renderRow.bind(this));
},
/**
* A 'selector' is the small checkbox on the left of a record in a list
@@ -638,52 +640,60 @@ var ListRenderer = BasicRenderer.extend({
*
* @override
* @private
- * returns {Deferred} this deferred is resolved immediately
+ * @returns {Promise} resolved when the view has been rendered
*/
_renderView: function () {
var self = this;
- this.$el
- .removeClass('table-responsive')
- .empty();
-
- // destroy the previously instantiated pagers, if any
- _.invoke(this.pagers, 'destroy');
+ var oldPagers = this.pagers;
this.pagers = [];
- var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
// display the no content helper if there is no data to display
+ var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
if (displayNoContentHelper) {
+ // destroy the previously instantiated pagers, if any
+ _.invoke(oldPagers, 'destroy');
+
+ this.$el.removeClass('table-responsive');
this.$el.html(this._renderNoContentHelper());
- return this._super();
+ return this._super.apply(this, arguments);
}
- var $table = $('<table>').addClass('o_list_view table table-sm table-hover table-striped');
- this.$el.addClass('table-responsive')
- .append($table);
+ var orderedBy = this.state.orderedBy;
+ this.hasHandle = orderedBy.length === 0 || orderedBy[0].name === this.handleField;
this._computeAggregates();
+
+ var $table = $('<table>').addClass('o_list_view table table-sm table-hover table-striped');
$table.toggleClass('o_list_view_grouped', this.isGrouped);
$table.toggleClass('o_list_view_ungrouped', !this.isGrouped);
- this.hasHandle = this.state.orderedBy.length === 0 ||
- this.state.orderedBy[0].name === this.handleField;
+ var defs = [];
+ this.defs = defs;
if (this.isGrouped) {
- $table
- .append(this._renderHeader(true))
- .append(this._renderGroups(this.state.data))
- .append(this._renderFooter());
+ $table.append(this._renderHeader());
+ $table.append(this._renderGroups(this.state.data));
+ $table.append(this._renderFooter());
+
} else {
- $table
- .append(this._renderHeader())
- .append(this._renderBody())
- .append(this._renderFooter());
- }
- if (this.selection.length) {
- var $checked_rows = this.$('tr').filter(function (index, el) {
- return _.contains(self.selection, $(el).data('id'));
- });
- $checked_rows.find('.o_list_record_selector input').prop('checked', true);
+ $table.append(this._renderHeader());
+ $table.append(this._renderBody());
+ $table.append(this._renderFooter());
}
- return this._super();
+ delete this.defs;
+
+ var prom = Promise.all(defs).then(function () {
+ // destroy the previously instantiated pagers, if any
+ _.invoke(oldPagers, 'destroy');
+
+ self.$el.addClass('table-responsive').html($table);
+
+ if (self.selection.length) {
+ var $checked_rows = self.$('tr').filter(function (index, el) {
+ return _.contains(self.selection, $(el).data('id'));
+ });
+ $checked_rows.find('.o_list_record_selector input').prop('checked', true);
+ }
+ });
+ return Promise.all([this._super.apply(this, arguments), prom]);
},
/**
* Each line can be decorated according to a few simple rules. The arch
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 3f195c1ea24..a4a2bae8fb0 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -45,10 +45,10 @@ var PivotModel = AbstractModel.extend({
/**
* Close a header. This method is actually synchronous, but returns a
- * deferred.
+ * promise.
*
* @param {any} headerID
- * @returns {Deferred}
+ * @returns {Promise}
*/
closeHeader: function (headerID) {
var header = this.data.headers[headerID];
@@ -58,7 +58,7 @@ var PivotModel = AbstractModel.extend({
header.root.groupbys.splice(newGroupbyLength);
},
/**
- * @returns {Deferred}
+ * @returns {Promise}
*/
expandAll: function () {
return this._loadData();
@@ -118,8 +118,8 @@ var PivotModel = AbstractModel.extend({
});
}));
}
- return $.when.apply(null, defs).then(function () {
- var results = Array.prototype.slice.call(arguments);
+ return Promise.all(defs).then(function () {
+ var results = Array.prototype.slice.call(arguments[0]);
var data = [];
var comparisonData = [];
_.each(results, function (result) {
@@ -358,7 +358,7 @@ var PivotModel = AbstractModel.extend({
* @param {string[]} params.compare
* @param {Object} params.fields
* @param {string} params.default_order
- * @returns {Deferred}
+ * @returns {Promise}
*/
load: function (params) {
var self = this;
@@ -394,7 +394,7 @@ var PivotModel = AbstractModel.extend({
* @override
* @param {any} handle this parameter is ignored
* @param {Object} params
- * @returns {Deferred}
+ * @returns {Promise}
*/
reload: function (handle, params) {
var self = this;
@@ -507,14 +507,14 @@ var PivotModel = AbstractModel.extend({
* Toggle the active state for a given measure, then reload the data.
*
* @param {string} field
- * @returns {Deferred}
+ * @returns {Promise}
*/
toggleMeasure: function (field) {
if (_.contains(this.data.measures, field)) {
this.data.measures = _.without(this.data.measures, field);
// in this case, we already have all data in memory, no need to
// actually reload a lesser amount of information
- return $.when();
+ return Promise.resolve();
} else {
this.data.measures.push(field);
}
@@ -705,7 +705,7 @@ var PivotModel = AbstractModel.extend({
return result;
},
/**
- * @returns {Deferred}
+ * @returns {Promise}
*/
_loadData: function () {
var self = this;
@@ -755,8 +755,7 @@ var PivotModel = AbstractModel.extend({
}));
}
- return this._loadDataDropPrevious.add($.when.apply(null, defs)).then(function () {
- var results = Array.prototype.slice.call(arguments);
+ return this._loadDataDropPrevious.add(Promise.all(defs)).then(function (results) {
var data = [];
var comparisonData = [];
_.each(results, function (result) {
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index 1a3c451f064..101f3734392 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -72,7 +72,7 @@ var PivotRenderer = AbstractRenderer.extend({
/**
* @override
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_render: function () {
if (!this._hasContent()) {
diff --git a/addons/web/static/src/js/views/standalone_field_manager_mixin.js b/addons/web/static/src/js/views/standalone_field_manager_mixin.js
index da3cc19350d..501ecf7c012 100644
--- a/addons/web/static/src/js/views/standalone_field_manager_mixin.js
+++ b/addons/web/static/src/js/views/standalone_field_manager_mixin.js
@@ -38,7 +38,7 @@ var StandaloneFieldManagerMixin = _.extend({}, FieldManagerMixin, {
* @param {string} id basicModel Id for the changed record
* @param {string[]} fields the fields (names) that have been changed
* @param {OdooEvent} event the event that triggered the change
- * @returns {Deferred}
+ * @returns {Promise}
*/
_confirmChange: function (id, fields, event) {
var result = FieldManagerMixin._confirmChange.apply(this, arguments);
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index befe4247c00..75be3a3d07e 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -128,7 +128,7 @@ var FormViewDialog = ViewDialog.extend({
text: (multi_select ? _t("Save & Close") : _t("Save")),
classes: "btn-primary",
click: function () {
- this._save().then(self.close.bind(self));
+ self._save().then(self.close.bind(self));
}
});
@@ -137,7 +137,7 @@ var FormViewDialog = ViewDialog.extend({
text: _t("Save & New"),
classes: "btn-primary",
click: function () {
- this._save()
+ self._save()
.then(self.form_view.createRecord.bind(self.form_view, self.parentID))
.then(function () {
if (!self.deletable) {
@@ -183,7 +183,7 @@ var FormViewDialog = ViewDialog.extend({
var FormView = view_registry.get('form');
var fields_view_def;
if (this.options.fields_view) {
- fields_view_def = $.when(this.options.fields_view);
+ fields_view_def = Promise.resolve(this.options.fields_view);
} else {
fields_view_def = this.loadFieldView(this.res_model, this.context, this.options.view_id, 'form');
}
@@ -213,9 +213,9 @@ var FormViewDialog = ViewDialog.extend({
if (self.recordID && self.shouldSaveLocally) {
self.model.save(self.recordID, {savePoint: true});
}
- self.form_view.appendTo(fragment)
+ return self.form_view.appendTo(fragment)
.then(function () {
- self.opened().always(function () {
+ self.opened().then(function () {
var $buttons = $('<div>');
self.form_view.renderButtons($buttons);
if ($buttons.children().length) {
@@ -226,7 +226,7 @@ var FormViewDialog = ViewDialog.extend({
in_DOM: true,
});
});
- _super();
+ return _super();
});
});
@@ -255,7 +255,7 @@ var FormViewDialog = ViewDialog.extend({
/**
* @private
- * @returns {Deferred}
+ * @returns {Promise}
*/
_save: function () {
var self = this;
@@ -332,7 +332,7 @@ var SelectCreateDialog = ViewDialog.extend({
}
var self = this;
var _super = this._super.bind(this);
- this.loadViews(this.res_model, this.context, [[false, 'list'], [false, 'search']], {})
+ return this.loadViews(this.res_model, this.context, [[false, 'list'], [false, 'search']], {})
.then(this.setup.bind(this))
.then(function (fragment) {
self.opened().then(function () {
@@ -342,9 +342,8 @@ var SelectCreateDialog = ViewDialog.extend({
});
self.set_buttons(self.__buttons);
});
- _super();
+ return _super();
});
- return this;
},
setup: function (fieldsViews) {
diff --git a/addons/web/static/src/js/views/view_utils.js b/addons/web/static/src/js/views/view_utils.js
index 94485c1401a..ea2270ce466 100644
--- a/addons/web/static/src/js/views/view_utils.js
+++ b/addons/web/static/src/js/views/view_utils.js
@@ -63,7 +63,7 @@ var viewUtils = {
*/
renderButtonFromNode: function (node, options) {
var btnOptions = {
- attrs: _.omit(node.attrs, 'icon', 'string', 'type', 'attrs', 'modifiers', 'options'),
+ attrs: _.omit(node.attrs, 'icon', 'string', 'type', 'attrs', 'modifiers', 'options', 'effect'),
icon: node.attrs.icon,
};
if (options && options.extraClass) {
diff --git a/addons/web/static/src/js/widgets/attach_document.js b/addons/web/static/src/js/widgets/attach_document.js
index 13496f3aea7..3175875c151 100644
--- a/addons/web/static/src/js/widgets/attach_document.js
+++ b/addons/web/static/src/js/widgets/attach_document.js
@@ -108,26 +108,27 @@ var AttachDocument = Widget.extend({
* @private
*/
_onFileLoaded: function () {
- var self = this,
- def = $.Deferred();
-
- if (self.node.attrs.action) {
- // the first argument isn't a file but the jQuery.Event
- var files = Array.prototype.slice.call(arguments, 1);
- self._rpc({
- model: self.res_model,
- method: self.node.attrs.action,
- args: [self.res_id],
- kwargs: {
- 'attachment_ids': _.map(files, function (file) {return file.id;}),
- }
- }).then( function () {
- def.resolve();
- });
- } else {
- def.resolve();
- }
- return $.when(def).then(function () {
+ var self = this;
+ return new Promise(function (resolve) {
+ if (self.node.attrs.action) {
+ // the first argument isn't a file but the jQuery.Event
+ var files = Array.prototype.slice.call(arguments, 1);
+ self._rpc({
+ model: self.res_model,
+ method: self.node.attrs.action,
+ args: [self.res_id],
+ kwargs: {
+ attachment_ids: _.map(files, function (file) {
+ return file.id;
+ }),
+ }
+ }).then(function () {
+ resolve();
+ });
+ } else {
+ resolve();
+ }
+ }).then(function () {
self.trigger_up('reload');
framework.unblockUI();
});
diff --git a/addons/web/static/src/js/widgets/change_password.js b/addons/web/static/src/js/widgets/change_password.js
index c05fa30408b..e75d8dfda38 100644
--- a/addons/web/static/src/js/widgets/change_password.js
+++ b/addons/web/static/src/js/widgets/change_password.js
@@ -20,7 +20,7 @@ var ChangePassword = AbstractAction.extend({
* @fixme: weird interaction with the parent for the $buttons handling
*
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
var self = this;
@@ -37,7 +37,7 @@ var ChangePassword = AbstractAction.extend({
fields: $('form[name=change_password_form]').serializeArray()
}
})
- .done(function (result) {
+ .then(function (result) {
if (result.error) {
self._display_error(result);
} else {
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index e24818f7336..a913fcd0ea2 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -56,7 +56,7 @@ var DataExport = Dialog.extend({
*/
start: function () {
var self = this;
- var defs = [this._super.apply(this, arguments)];
+ var proms = [this._super.apply(this, arguments)];
// The default for the ".modal_content" element is "max-height: 100%;"
// but we want it to always expand to "height: 100%;" for this modal.
@@ -67,10 +67,10 @@ var DataExport = Dialog.extend({
this.$fieldsList = this.$('.o_fields_list');
this.$importCompatRadios = this.$('.o_import_compat input');
- defs.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats));
- defs.push(this._onChangeCompatibleInput());
+ proms.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats));
+ proms.push(this._onChangeCompatibleInput());
- defs.push(this.getParent().getActiveDomain().then(function (domain) {
+ proms.push(this.getParent().getActiveDomain().then(function (domain) {
if (domain === undefined) {
self.idsToExport = self.getParent().getSelectedIds();
self.domain = self.record.domain;
@@ -80,7 +80,7 @@ var DataExport = Dialog.extend({
}
}));
- defs.push(this._showExportsList().then(function () {
+ proms.push(this._showExportsList().then(function () {
_.each(self.records, function (record, key) {
if (_.contains(self.defaultExportFields, key)) {
self._addField(key, record.string);
@@ -88,12 +88,12 @@ var DataExport = Dialog.extend({
});
}));
- return $.when.apply($, defs);
+ return Promise.all(proms);
- function doSetupExportFormats (formats) {
+ function doSetupExportFormats(formats) {
var $fmts = self.$('.o_export_format');
- _.each(formats, function (format, i) {
+ _.each(formats, function (format) {
var $radio = $('<input/>', {type: 'radio', value: format.tag, name: 'o_export_format_name'});
var $label = $('<span/>', {html: format.label});
@@ -192,16 +192,16 @@ var DataExport = Dialog.extend({
*/
_onExpandAction: function (record) {
var self = this;
- if (!record['children']) {
+ if (!record.children) {
return;
}
- var model = record['params']['model'];
- var prefix = record['params']['prefix'];
- var name = record['params']['name'];
+ var model = record.params.model;
+ var prefix = record.params.prefix;
+ var name = record.params.name;
var excludeFields = [];
- if (record['relation_field']) {
- excludeFields.push(record['relation_field']);
+ if (record.relation_field) {
+ excludeFields.push(record.relation_field);
}
if (!record.loaded) {
@@ -212,11 +212,11 @@ var DataExport = Dialog.extend({
prefix: prefix,
parent_name: name,
import_compat: !!this.$importCompatRadios.filter(':checked').val(),
- parent_field_type: record['field_type'],
- parent_field: record['params']['parent_field'],
+ parent_field_type: record.field_type,
+ parent_field: record.params.parent_field,
exclude: excludeFields,
},
- }).done(function (results) {
+ }).then(function (results) {
record.loaded = true;
self._onShowData(results, record.id);
});
@@ -294,7 +294,7 @@ var DataExport = Dialog.extend({
var self = this;
if (this.$('.o_exported_lists_select').is(':hidden')) {
this.$('.o_exported_lists').show();
- return $.when();
+ return Promise.resolve();
}
return this._rpc({
@@ -355,7 +355,7 @@ var DataExport = Dialog.extend({
model: this.record.model,
import_compat: this.isCompatibleMode,
},
- }).done(function (records) {
+ }).then(function (records) {
var compatibleFields = _.map(records, function (record) { return record.id; });
self.$fieldsList
.find('option')
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js
index 8e20e1ce18d..ed71679699c 100644
--- a/addons/web/static/src/js/widgets/domain_selector.js
+++ b/addons/web/static/src/js/widgets/domain_selector.js
@@ -188,11 +188,14 @@ var DomainTree = DomainNode.extend({
},
/**
* @see DomainNode.start
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this._postRender();
- return $.when(this._super.apply(this, arguments), this._renderChildrenTo(this.$childrenContainer));
+ return Promise.all([
+ this._super.apply(this, arguments),
+ this._renderChildrenTo(this.$childrenContainer)
+ ]);
},
//--------------------------------------------------------------------------
@@ -369,11 +372,11 @@ var DomainTree = DomainNode.extend({
*
* @private
* @param {jQuery} $to - the jQuery node to which the children must be added
- * @returns {Deferred}
+ * @returns {Promise}
*/
_renderChildrenTo: function ($to) {
var $div = $("<div/>");
- return $.when.apply($, _.map(this.children, (function (child) {
+ return Promise.all(_.map(this.children, (function (child) {
return child.appendTo($div);
}).bind(this))).then((function () {
_.each(this.children, function (child) {
@@ -485,11 +488,11 @@ var DomainSelector = DomainTree.extend({
* does nothing.
*
* @param {string} domain
- * @returns {Deferred} resolved when the rerendering is finished
+ * @returns {Promise} resolved when the rerendering is finished
*/
setDomain: function (domain) {
if (domain === Domain.prototype.arrayToString(this.getDomain())) {
- return $.when();
+ return Promise.resolve();
}
var parsedDomain = this._parseDomain(domain);
if (parsedDomain) {
@@ -542,7 +545,7 @@ var DomainSelector = DomainTree.extend({
* This method is ugly but achieves the right behavior without flickering.
*
* @param {Array|string} domain
- * @returns {Deferred}
+ * @returns {Promise}
*/
_redraw: function (domain) {
var oldChildren = this.children.slice();
@@ -637,7 +640,7 @@ var DomainLeaf = DomainNode.extend({
* Prepares the information the rendering of the widget will need by
* pre-instantiating its internal field selector widget.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
willStart: function () {
var defs = [this._super.apply(this, arguments)];
@@ -702,17 +705,17 @@ var DomainLeaf = DomainNode.extend({
}).bind(this)));
}
- return $.when.apply($, wDefs);
+ return Promise.all(wDefs);
}
}).bind(this)));
- return $.when.apply($, defs);
+ return Promise.all(defs);
},
/**
* @see DomainNode.start
* Appends the prepared field selector and value widget.
*
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_info, > .o_domain_leaf_edition")); // place the field selector
diff --git a/addons/web/static/src/js/widgets/domain_selector_dialog.js b/addons/web/static/src/js/widgets/domain_selector_dialog.js
index 46b54089c5f..f6fe7f0c479 100644
--- a/addons/web/static/src/js/widgets/domain_selector_dialog.js
+++ b/addons/web/static/src/js/widgets/domain_selector_dialog.js
@@ -45,10 +45,10 @@ return Dialog.extend({
// this restores default modal height (bootstrap) and allows field selector to overflow
self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto');
});
- return $.when(
+ return Promise.all([
this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el)
- );
+ ]);
},
});
});
diff --git a/addons/web/static/src/js/widgets/iframe_widget.js b/addons/web/static/src/js/widgets/iframe_widget.js
index 77d27fac5b4..8ecbd5f1c7d 100644
--- a/addons/web/static/src/js/widgets/iframe_widget.js
+++ b/addons/web/static/src/js/widgets/iframe_widget.js
@@ -29,7 +29,7 @@ var IFrameWidget = Widget.extend({
},
/**
* @override
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this.$el.css({height: '100%', width: '100%', border: 0});
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js
index b8c953a2d0e..08a7ae47c81 100644
--- a/addons/web/static/src/js/widgets/model_field_selector.js
+++ b/addons/web/static/src/js/widgets/model_field_selector.js
@@ -112,17 +112,17 @@ var ModelFieldSelector = Widget.extend({
},
/**
* @see Widget.willStart()
- * @returns {Deferred}
+ * @returns {Promise}
*/
willStart: function () {
- return $.when(
+ return Promise.all([
this._super.apply(this, arguments),
this._prefill()
- );
+ ]);
},
/**
* @see Widget.start
- * @returns {Deferred}
+ * @returns {Promise}
*/
start: function () {
this.$value = this.$(".o_field_selector_value");
@@ -162,11 +162,11 @@ var ModelFieldSelector = Widget.extend({
* Saves a new field chain (array) and re-render.
*
* @param {string[]} chain - the new field chain
- * @returns {Deferred} resolved once the re-rendering is finished
+ * @returns {Promise} resolved once the re-rendering is finished
*/
setChain: function (chain) {
if (_.isEqual(chain, this.chain)) {
- return $.when();
+ return Promise.resolve();
}
this.chain = chain;
@@ -290,14 +290,14 @@ var ModelFieldSelector = Widget.extend({
* chain.
*
* @private
- * @returns {Deferred} resolved once the whole field chain has been
+ * @returns {Promise} resolved once the whole field chain has been
* processed
*/
_prefill: function () {
this.pages = [];
return this._pushPageData(this.model).then((function () {
this._validate(true);
- return (this.chain.length ? processChain.call(this, this.chain.slice().reverse()) : $.when());
+ return (this.chain.length ? processChain.call(this, this.chain.slice().reverse()) : Promise.resolve());
}).bind(this));
function processChain(chain) {
@@ -305,11 +305,11 @@ var ModelFieldSelector = Widget.extend({
if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible
return this._pushPageData(field.relation).then(processChain.bind(this, chain));
} else if (field && chain.length === 0) { // Last node fetched
- return $.when();
+ return Promise.resolve();
} else { // Wrong node chain
this._validate(false);
}
- return $.when();
+ return Promise.resolve();
}
},
/**
@@ -318,12 +318,12 @@ var ModelFieldSelector = Widget.extend({
*
* @private
* @param {string} model - the model name whose fields have to be fetched
- * @returns {Deferred} resolved once the fields have been added
+ * @returns {Promise} resolved once the fields have been added
*/
_pushPageData: function (model) {
var def;
if (this.model === model && this.options.fields) {
- def = $.when(sortFields(this.options.fields, model, this.options.order));
+ def = Promise.resolve(sortFields(this.options.fields, model, this.options.order));
} else {
def = this._getModelFieldsFromCache(model, this.options.filters);
}
diff --git a/addons/web/static/src/js/widgets/name_and_signature.js b/addons/web/static/src/js/widgets/name_and_signature.js
index c860b0a4361..7c3f05a7148 100644
--- a/addons/web/static/src/js/widgets/name_and_signature.js
+++ b/addons/web/static/src/js/widgets/name_and_signature.js
@@ -80,14 +80,12 @@ var NameAndSignature = Widget.extend({
*/
willStart: function () {
var self = this;
-
- return $.when(
+ return Promise.all([
this._super.apply(this, arguments),
this._rpc({route: '/web/sign/get_fonts/' + self.defaultFont}).then(function (data) {
self.fonts = data;
})
- );
-
+ ]);
},
/**
* Finds the DOM elements, initializes the signature area,
@@ -217,7 +215,7 @@ var NameAndSignature = Widget.extend({
resetSignature: function () {
if (!this.$signatureField) {
// no action if called before start
- return $.Deferred().reject();
+ return Promise.reject();
}
// recompute size based on the current width
this.$signatureField.css({width: 'unset'});
@@ -248,7 +246,7 @@ var NameAndSignature = Widget.extend({
this.focusName();
- return $.Deferred().resolve();
+ return Promise.resolve();
},
/**
* Changes the signature mode. Toggles the display of the relevant
diff --git a/addons/web/static/src/js/widgets/pie_chart.js b/addons/web/static/src/js/widgets/pie_chart.js
index 44a08a5a22a..ddeca1fdcec 100644
--- a/addons/web/static/src/js/widgets/pie_chart.js
+++ b/addons/web/static/src/js/widgets/pie_chart.js
@@ -67,7 +67,7 @@ var PieChart = Widget.extend({
self.controller = controller;
return self.controller.appendTo(document.createDocumentFragment());
});
- return $.when(def1, def2);
+ return Promise.all([def1, def2]);
},
/**
* @override
diff --git a/addons/web/static/src/js/widgets/switch_company_menu.js b/addons/web/static/src/js/widgets/switch_company_menu.js
index cc0f8825643..a389ee268cd 100644
--- a/addons/web/static/src/js/widgets/switch_company_menu.js
+++ b/addons/web/static/src/js/widgets/switch_company_menu.js
@@ -31,12 +31,6 @@ var SwitchCompanyMenu = Widget.extend({
/**
* @override
*/
- willStart: function () {
- return session.user_companies ? this._super() : $.Deferred().reject();
- },
- /**
- * @override
- */
start: function () {
var companiesList = '';
if (this.isMobile) {
@@ -81,9 +75,9 @@ var SwitchCompanyMenu = Widget.extend({
});
},
});
-
-SystrayMenu.Items.push(SwitchCompanyMenu);
-
+if (session.user_companies) {
+ SystrayMenu.Items.push(SwitchCompanyMenu);
+}
return SwitchCompanyMenu;
});