[REF] web: adapt code after jQuery update

Part of task 1896658

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>
This commit is contained in:
Vincent Schippefilt
2019-03-06 20:07:17 +01:00
committed by Aaron Bohy
co-authored by Aaron Bohy Christophe Matthieu Mathieu Duckerts-Antoine David Monjoie Martin Geubelle svs-odoo
parent eeed894fd5
commit ab56e637b7
156 changed files with 11415 additions and 10825 deletions
+4 -3
View File
@@ -83,18 +83,19 @@ method.
As required by :js:class:`~openerp.web.Widget`'s contract, if As required by :js:class:`~openerp.web.Widget`'s contract, if
:js:func:`~openerp.web.Widget.start()` executes any asynchronous :js:func:`~openerp.web.Widget.start()` executes any asynchronous
code it should return a ``$.Deferred`` so callers know when it's code it should return a ``Promise`` so callers know when it's
ready for interaction. ready for interaction.
.. code-block:: javascript .. code-block:: javascript
start: function () { start: function () {
return $.when( return Promise.all([
this._super(), this._super(),
// Simply read the res.widget object this action should display // Simply read the res.widget object this action should display
new instance.web.Model('res.widget').call( new instance.web.Model('res.widget').call(
'read', [[this.widget_id], ['title']]) 'read', [[this.widget_id], ['title']])
.then(this.proxy('on_widget_loaded')); .then(this.proxy('on_widget_loaded')
]);
} }
The client action can then behave exactly as it wishes to within its The client action can then behave exactly as it wishes to within its
+44 -31
View File
@@ -101,7 +101,7 @@ var Query = Class.extend({
* Fetches the first record matching the query, or null * Fetches the first record matching the query, or null
* *
* @param {Object} [options] additional options for the rpc() method * @param {Object} [options] additional options for the rpc() method
* @returns {jQuery.Deferred<Object|null>} * @returns {Promise<Object|null>}
*/ */
first: function (options) { first: function (options) {
var self = this; var self = this;
@@ -115,7 +115,7 @@ var Query = Class.extend({
* Fetches all records matching the query * Fetches all records matching the query
* *
* @param {Object} [options] additional options for the rpc() method * @param {Object} [options] additional options for the rpc() method
* @returns {jQuery.Deferred<Array<>>} * @returns {Promise<Array<>>}
*/ */
all: function (options) { all: function (options) {
return this._execute(options); return this._execute(options);
@@ -123,10 +123,10 @@ var Query = Class.extend({
/** /**
* Fetches the number of records matching the query in the database * Fetches the number of records matching the query in the database
* *
* @returns {jQuery.Deferred<Number>} * @returns {Promise<Number>}
*/ */
count: function () { count: function () {
if (this._count !== undefined) { return $.when(this._count); } if (this._count !== undefined) { return Promise.resolve(this._count); }
return this._model.call( return this._model.call(
'search_count', [this._filter], { 'search_count', [this._filter], {
context: this._model.context(this._context)}); 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} ids identifiers of the records to read
* @param {Array} [fields] fields to read and return, by default all fields are returned * @param {Array} [fields] fields to read and return, by default all fields are returned
* @param {Object} [options] * @param {Object} [options]
* @returns {$.Deferred} * @returns {Promise}
*/ */
read_ids: function (ids, fields, options) { read_ids: function (ids, fields, options) {
if (_.isEmpty(ids)) if (_.isEmpty(ids)) {
return $.Deferred().resolve([]); return Promise.resolve([]);
}
options = options || {}; options = options || {};
var method = 'read'; var method = 'read';
@@ -405,7 +406,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @params {Object} [options] * @params {Object} [options]
* @param {Number} [options.offset=0] The index from which selected records should be returned * @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 * @param {Number} [options.limit=null] The maximum number of records to return
* @returns {$.Deferred} * @returns {Promise}
*/ */
read_slice: function (fields, options) { read_slice: function (fields, options) {
var self = this; var self = this;
@@ -414,21 +415,23 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
.limit(options.limit || false) .limit(options.limit || false)
.offset(options.offset || 0) .offset(options.offset || 0)
.all(); .all();
return this.orderer.add(query).done(function (records) { var prom = this.orderer.add(query);
prom.then(function (records) {
self.ids = _(records).pluck('id'); self.ids = _(records).pluck('id');
}); });
return prom;
}, },
/** /**
* Reads the current dataset record (from its index) * Reads the current dataset record (from its index)
* *
* @params {Array} [fields] fields to read and return, by default all fields are returned * @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 * @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) { read_index: function (fields, options) {
options = options || {}; options = options || {};
return this.read_ids([this.ids[this.index]], fields, options).then(function (records) { 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]; 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 {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 * @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) { default_get: function (fields, options) {
options = options || {}; options = options || {};
@@ -451,15 +454,17 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* @param {Object} options Dictionary that can contain the following keys: * @param {Object} options Dictionary that can contain the following keys:
* - readonly_fields: Values from readonly fields that were updated by * - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred} * @returns {Promise}
*/ */
create: function (data, options) { create: function (data, options) {
var self = this; var self = this;
return this._model.call('create', [data], { var prom = this._model.call('create', [data], {
context: this.get_context() context: this.get_context()
}).done(function () { });
prom.then(function () {
self.trigger('dataset_changed', data, options); self.trigger('dataset_changed', data, options);
}); });
return prom;
}, },
/** /**
* Saves the provided data in an existing db record * 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. * - context: The context to use in the server-side call.
* - readonly_fields: Values from readonly fields that were updated by * - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred} * @returns {Promise}
*/ */
write: function (id, data, options) { write: function (id, data, options) {
options = options || {}; options = options || {};
var self = this; var self = this;
return this._model.call('write', [[id], data], { var prom = this._model.call('write', [[id], data], {
context: this.get_context(options.context) context: this.get_context(options.context)
}).done(function () { });
prom.then(function () {
self.trigger('dataset_changed', id, data, options); self.trigger('dataset_changed', id, data, options);
}); });
return prom;
}, },
/** /**
* Deletes an existing record from the database * Deletes an existing record from the database
@@ -488,11 +495,13 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
*/ */
unlink: function (ids) { unlink: function (ids) {
var self = this; var self = this;
return this._model.call('unlink', [ids], { var prom = this._model.call('unlink', [ids], {
context: this.get_context() context: this.get_context()
}).done(function () { });
prom.then(function () {
self.trigger('dataset_changed', ids); self.trigger('dataset_changed', ids);
}); });
return prom;
}, },
/** /**
* Calls an arbitrary RPC method * 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 {Array} [args] arguments to pass to the method
* @param {Function} callback * @param {Function} callback
* @param {Function} error_callback * @param {Function} error_callback
* @returns {$.Deferred} * @returns {Promise}
*/ */
call: function (method, args) { call: function (method, args) {
return this._model.call(method, args); return this._model.call(method, args);
@@ -511,7 +520,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* *
* @param {String} method * @param {String} method
* @param {Array} [args] * @param {Array} [args]
* @returns {$.Deferred} * @returns {Promise}
*/ */
call_button: function (method, args) { call_button: function (method, args) {
return this._model.call_button(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 * Fetches the "readable name" for records, based on intrinsic rules
* *
* @param {Array} ids * @param {Array} ids
* @returns {$.Deferred} * @returns {Promise}
*/ */
name_get: function (ids) { name_get: function (ids) {
return this._model.call('name_get', [ids], {context: this.get_context()}); 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 {String} [operator='ilike'] matching operator to use with the provided name value
* @param {Number} [limit=0] maximum number of matches to return * @param {Number} [limit=0] maximum number of matches to return
* @param {Function} callback function to call with name_search result * @param {Function} callback function to call with name_search result
* @returns {$.Deferred} * @returns {Promise}
*/ */
name_search: function (name, domain, operator, limit) { name_search: function (name, domain, operator, limit) {
return this._model.call('name_search', { return this._model.call('name_search', {
@@ -603,7 +612,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
* Resequence records. * Resequence records.
* *
* @param {Array} ids identifiers of the records to resequence * @param {Array} ids identifiers of the records to resequence
* @returns {$.Deferred} * @returns {Promise}
*/ */
resequence: function (ids, options) { resequence: function (ids, options) {
options = options || {}; options = options || {};
@@ -643,7 +652,7 @@ var DataSetStatic = DataSet.extend({
unlink: function (ids) { unlink: function (ids) {
this.set_ids(_.without.apply(null, [this.ids].concat(ids))); this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
this.trigger('unlink', 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 {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.offset=0] The index from which selected records should be returned
* @param {Number} [options.limit=null] The maximum number of records to return * @param {Number} [options.limit=null] The maximum number of records to return
* @returns {$.Deferred} * @returns {Promise}
*/ */
read_slice: function (fields, options) { read_slice: function (fields, options) {
options = options || {}; options = options || {};
@@ -686,11 +695,13 @@ var DataSetSearch = DataSet.extend({
.limit(options.limit || false); .limit(options.limit || false);
q = q.order_by.apply(q, this._sort); 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 // 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'); self.ids = _(records).pluck('id');
}); });
return prom;
}, },
get_domain: function (other_domain) { get_domain: function (other_domain) {
return this._model.domain(other_domain); return this._model.domain(other_domain);
@@ -717,10 +728,12 @@ var DataSetSearch = DataSet.extend({
}, },
unlink: function (ids, callback, error_callback) { unlink: function (ids, callback, error_callback) {
var self = this; var self = this;
return this._super(ids).done(function () { var prom = this._super(ids);
prom.then(function () {
self.remove_ids( ids); self.remove_ids( ids);
self.trigger("dataset_changed", ids, callback, error_callback); self.trigger("dataset_changed", ids, callback, error_callback);
}); });
return prom;
}, },
size: function () { size: function () {
if (this._length !== null) { if (this._length !== null) {
@@ -801,7 +814,7 @@ var Model = Class.extend({
* @param {Array} [args] positipyEvalonal arguments * @param {Array} [args] positipyEvalonal arguments
* @param {Object} [kwargs] keyword arguments * @param {Object} [kwargs] keyword arguments
* @param {Object} [options] additional options for the rpc() method * @param {Object} [options] additional options for the rpc() method
* @returns {jQuery.Deferred<>} call result * @returns {Promise<>} call result
*/ */
call: function (method, args, kwargs, options) { call: function (method, args, kwargs, options) {
args = args || []; args = args || [];
+51 -52
View File
@@ -22,20 +22,21 @@ var Apps = AbstractAction.extend({
}, },
get_client: function() { 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. // the remote host is available or not.
var check_client_available = function(client) { var check_client_available = function(client) {
var d = $.Deferred();
var i = new Image(); var i = new Image();
i.onerror = function() { var def = new Promise(function (resolve, reject) {
d.reject(client); i.onerror = function() {
}; reject(client);
i.onload = function() { };
d.resolve(client); i.onload = function() {
}; resolve(client);
};
});
var ts = new Date().getTime(); var ts = new Date().getTime();
i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif?%s', client.origin, ts); i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif?%s', client.origin, ts);
return d.promise(); return def;
}; };
if (apps_client) { if (apps_client) {
return check_client_available(apps_client); return check_client_available(apps_client);
@@ -105,58 +106,56 @@ var Apps = AbstractAction.extend({
start: function() { start: function() {
var self = this; var self = this;
var def = $.Deferred(); return new Promise(function (resolve, reject) {
self.get_client().then(function(client) { self.get_client().then(function (client) {
self.client = client; self.client = client;
var qs = {db: client.dbname}; var qs = {db: client.dbname};
if (session.debug) { if (session.debug) {
qs.debug = session.debug; qs.debug = session.debug;
} }
var u = $.param.querystring(client.origin + "/apps/embed/client", qs); var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
var css = {width: '100%', height: '750px'}; var css = {width: '100%', height: '750px'};
self.$ifr = $('<iframe>').attr('src', u); self.$ifr = $('<iframe>').attr('src', u);
self.uniq = _.uniqueId('apps'); self.uniq = _.uniqueId('apps');
$(window).on("message." + self.uniq, self.proxy('_on_message')); $(window).on("message." + self.uniq, self.proxy('_on_message'));
self.on('message:ready', self, function(m) { self.on('message:ready', self, function(m) {
var w = this.$ifr[0].contentWindow; var w = this.$ifr[0].contentWindow;
var act = { var act = {
type: 'ir.actions.client', type: 'ir.actions.client',
tag: this.remote_action_tag, tag: this.remote_action_tag,
params: _.extend({}, this.params, { params: _.extend({}, this.params, {
db: session.db, db: session.db,
origin: session.origin, origin: session.origin,
}) })
}; };
w.postMessage({type:'action', action: act}, client.origin); w.postMessage({type:'action', action: act}, client.origin);
}); });
self.on('message:set_height', self, function(m) { self.on('message:set_height', self, function(m) {
this.$ifr.height(m.height); this.$ifr.height(m.height);
}); });
self.on('message:blockUI', self, function() { framework.blockUI(); }); self.on('message:blockUI', self, function() { framework.blockUI(); });
self.on('message:unblockUI', self, function() { framework.unblockUI(); }); 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.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'); self.$ifr.appendTo(self.$('.o_content')).css(css).addClass('apps-client');
def.resolve(); resolve();
}, function() { }, function() {
self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true); self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
return self._rpc({ return self._rpc({
route: '/web/action/load', route: '/web/action/load',
params: {action_id: self.failback_action_id}, params: {action_id: self.failback_action_id},
}).then(function(action) { }).then(function(action) {
return self.do_action(action); return self.do_action(action);
}).always(function () { }).then(reject, reject);
def.reject();
}); });
}); });
return def; }
},
}); });
var AppsUpdates = Apps.extend({ var AppsUpdates = Apps.extend({
+57 -51
View File
@@ -6,8 +6,8 @@
* @name openerp * @name openerp
* @namespace openerp * @namespace openerp
* *
* Each module can return a deferred. In that case, the module is marked as loaded * Each module can return a promise. In that case, the module is marked as loaded
* only when the deferred is resolved, and its value is equal to the resolved value. * 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. * The module can be rejected (unloaded). This will be logged in the console as info.
* *
* logs: * logs:
@@ -17,7 +17,7 @@
* Failed modules: * Failed modules:
* A javascript error is detected * A javascript error is detected
* Rejected modules: * 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. * is not loaded.
* Rejected linked modules: * Rejected linked modules:
* Modules who depend on a rejected module * Modules who depend on a rejected module
@@ -27,14 +27,14 @@
* Non loaded or failed module informations for debugging * Non loaded or failed module informations for debugging
*/ */
(function() { (function () {
"use strict"; "use strict";
var jobs = []; var jobs = [];
var factories = Object.create(null); var factories = Object.create(null);
var job_names = []; var job_names = [];
var job_deps = []; var job_deps = [];
var job_deferred = []; var job_promise = [];
var services = Object.create({}); var services = Object.create({});
@@ -45,15 +45,20 @@
var debug = ($.deparam($.param.querystring()).debug !== undefined); var debug = ($.deparam($.param.querystring()).debug !== undefined);
var odoo = window.odoo = window.odoo || {}; var odoo = window.odoo = window.odoo || {};
var didLogInfoResolve;
var didLogInfoPromise = new Promise(function (resolve) {
didLogInfoResolve = resolve;
});
_.extend(odoo, { _.extend(odoo, {
testing: typeof QUnit === "object", testing: typeof QUnit === "object",
debug: debug, debug: debug,
remaining_jobs: jobs, remaining_jobs: jobs,
__DEBUG__: { __DEBUG__: {
didLogInfo: $.Deferred(), didLogInfo: didLogInfoPromise,
get_dependencies: function (name, transitive) { get_dependencies: function (name, transitive) {
var deps = name instanceof Array ? name: [name], var deps = name instanceof Array ? name : [name],
changed; changed;
do { do {
changed = false; changed = false;
@@ -67,26 +72,26 @@
return deps; return deps;
}, },
get_dependents: function (name) { get_dependents: function (name) {
return _.pluck(_.where(job_deps, {from: name}), 'to'); return _.pluck(_.where(job_deps, { from: name }), 'to');
}, },
get_waited_jobs: function () { 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 () { get_missing_jobs: function () {
var self = this; var self = this;
var waited = this.get_waited_jobs(); var waited = this.get_waited_jobs();
var missing = []; var missing = [];
_.each(waited, function(job) { _.each(waited, function (job) {
_.each(self.get_dependencies(job), function(job) { _.each(self.get_dependencies(job), function (job) {
if (!(job in self.services)) { if (!(job in self.services)) {
missing.push(job); 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 () { get_failed_jobs: function () {
return _.filter(jobs, function (job) {return !!job.error;}); return _.filter(jobs, function (job) { return !!job.error; });
}, },
factories: factories, factories: factories,
services: services, services: services,
@@ -109,10 +114,10 @@
if (odoo.debug) { if (odoo.debug) {
if (!(deps instanceof Array)) { 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') { 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') { if (typeof name !== 'string') {
throw new Error("Invalid name definition (should be a string", name); throw new Error("Invalid name definition (should be a string", name);
@@ -133,7 +138,7 @@
job_names.push(name); job_names.push(name);
_.each(deps, function (dep) { _.each(deps, function (dep) {
job_deps.push({from:dep, to:name}); job_deps.push({ from: dep, to: name });
}); });
this.process_jobs(jobs, services); this.process_jobs(jobs, services);
@@ -149,7 +154,7 @@
var job; var job;
var jobdep; var jobdep;
for (var k=0; k<jobs.length; k++) { for (var k = 0; k < jobs.length; k++) {
debug_jobs[jobs[k].name] = job = { debug_jobs[jobs[k].name] = job = {
dependencies: jobs[k].deps, dependencies: jobs[k].deps,
dependents: odoo.__DEBUG__.get_dependents(jobs[k].name), dependents: odoo.__DEBUG__.get_dependents(jobs[k].name),
@@ -162,10 +167,10 @@
job.rejected = jobs[k].rejected; job.rejected = jobs[k].rejected;
rejected.push(job.name); rejected.push(job.name);
} }
var deps = odoo.__DEBUG__.get_dependencies( job.name ); var deps = odoo.__DEBUG__.get_dependencies(job.name);
for (var i=0; i<deps.length; i++) { for (var i = 0; i < deps.length; i++) {
if (job.name !== deps[i] && !(deps[i] in services)) { 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 (jobdep && jobdep.rejected) {
if (!job.rejected) { if (!job.rejected) {
job.rejected = []; job.rejected = [];
@@ -212,48 +217,49 @@
missing: missing, missing: missing,
failed: _.pluck(failed, 'name'), failed: _.pluck(failed, 'name'),
}; };
odoo.__DEBUG__.didLogInfo.resolve(); didLogInfoResolve();
}, },
process_jobs: function (jobs, services) { process_jobs: function (jobs, services) {
var job; var job;
function process_job (job) { function process_job(job) {
var require = make_require(job); var require = make_require(job);
var job_exec; var job_exec;
var def = $.Deferred(); var def = new Promise(function (resolve) {
try { try {
job_exec = job.factory.call(null, require); job_exec = job.factory.call(null, require);
jobs.splice(jobs.indexOf(job), 1); jobs.splice(jobs.indexOf(job), 1);
job_deferred.push(def); } catch (e) {
} catch (e) { job.error = e;
job.error = e; console.error('Error while loading ' + job.name);
console.error('Error while loading ' + job.name); console.error(e.stack);
console.error(e.stack); }
} if (!job.error) {
if (!job.error) { Promise.resolve(job_exec).then(
$.when(job_exec).then( function (data) {
function (data) { services[job.name] = data;
services[job.name] = data; resolve();
def.resolve(); odoo.process_jobs(jobs, services);
odoo.process_jobs(jobs, services); }).guardedCatch(function (e) {
}, function (e) { job.rejected = e || true;
job.rejected = e || true; jobs.push(job);
jobs.push(job); resolve();
def.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; }); 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); var deps = _.pick(services, job.deps);
function require (name) { function require(name) {
if (!(name in deps)) { if (!(name in deps)) {
console.error('Undefined dependency: ', name); console.error('Undefined dependency: ', name);
} else { } else {
@@ -278,9 +284,9 @@
// automatically log errors detected when loading modules // automatically log errors detected when loading modules
var log_when_loaded = function () { var log_when_loaded = function () {
_.delay(function () { _.delay(function () {
var len = job_deferred.length; var len = job_promise.length;
$.when.apply($, job_deferred).then(function () { Promise.all(job_promise).then(function () {
if (len === job_deferred.length) { if (len === job_promise.length) {
odoo.log(); odoo.log();
} else { } else {
log_when_loaded(); log_when_loaded();
@@ -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); var controlPanelView = new self.config.ControlPanelView(params);
return controlPanelView.getController(self).then(function (controlPanel) { return controlPanelView.getController(self).then(function (controlPanel) {
self._controlPanel = controlPanel; self._controlPanel = controlPanel;
@@ -112,7 +112,7 @@ var AbstractAction = Widget.extend(ActionMixin, {
}); });
}); });
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* @override * @override
@@ -78,7 +78,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
if (event.data.on_success) { if (event.data.on_success) {
event.data.on_success(result); event.data.on_success(result);
} }
}).fail(function (result) { }).guardedCatch(function (result) {
if (event.data.on_fail) { if (event.data.on_fail) {
event.data.on_fail(result); event.data.on_fail(result);
} }
@@ -116,17 +116,17 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
.then(function () { .then(function () {
self.$el.toggleClass('o_rtl', _t.database.parameters.direction === "rtl"); self.$el.toggleClass('o_rtl', _t.database.parameters.direction === "rtl");
self.bind_events(); self.bind_events();
return $.when( return Promise.all([
self.set_action_manager(), self.set_action_manager(),
self.set_loading() self.set_loading()
); ]);
}).then(function () { }).then(function () {
if (session.session_is_valid()) { if (session.session_is_valid()) {
return self.show_application(); return self.show_application();
} else { } else {
// database manager needs the webclient to keep going even // database manager needs the webclient to keep going even
// though it has no valid session // though it has no valid session
return $.when(); return Promise.resolve();
} }
}).then(function () { }).then(function () {
// Listen to 'scroll' event and propagate it on main bus // 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.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function () {
$(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter'); $(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) { this.$el.on('click', '.oe_dropdown_toggle', function (ev) {
ev.preventDefault(); ev.preventDefault();
var $toggle = $(this); var $toggle = $(this);
var doc_width = $(document).width(); var doc_width = $(document).width();
var $menu = $toggle.siblings('.oe_dropdown_menu'); 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'); var state = $menu.is('.oe_opened');
setTimeout(function () { setTimeout(function () {
// Do not alter propagation // Do not alter propagation
@@ -92,7 +92,7 @@ var ActionManager = Widget.extend({
* will be restored. It ensures that the current controller can be left (for * will be restored. It ensures that the current controller can be left (for
* instance, that it has no unsaved changes). * 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. * rejected otherwise.
*/ */
clearUncommittedChanges: function () { clearUncommittedChanges: function () {
@@ -100,7 +100,7 @@ var ActionManager = Widget.extend({
if (currentController) { if (currentController) {
return currentController.widget.canBeRemoved(); return currentController.widget.canBeRemoved();
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* This is the entry point to execute Odoo actions, given as an ID in * 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()) * is useful when we come from a loadState())
* @param {boolean} [options.replace_last_action=false] set to true to * @param {boolean} [options.replace_last_action=false] set to true to
* replace last part of the breadcrumbs with the action * 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 * 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 * executed (e.g. if doAction has been called to execute another action
* before this one was complete). * 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 // action.target 'main' is equivalent to 'current' except that it
// also clears the breadcrumbs // also clears the breadcrumbs
options.clear_breadcrumbs = action.target === 'main' || options.clear_breadcrumbs = action.target === 'main' ||
@@ -211,12 +211,12 @@ var ActionManager = Widget.extend({
* @param {Object} state * @param {Object} state
* @param {integer|string} [state.action] the action to execute (given its * @param {integer|string} [state.action] the action to execute (given its
* id or tag for client actions) * 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) { loadState: function (state) {
var action; var action;
if (!state.action) { if (!state.action) {
return $.when(); return Promise.resolve();
} }
if (_.isString(state.action) && core.action_registry.contains(state.action)) { if (_.isString(state.action) && core.action_registry.contains(state.action)) {
action = { action = {
@@ -299,7 +299,7 @@ var ActionManager = Widget.extend({
* @param {string} [action.target="current"] set to "new" to render the * @param {string} [action.target="current"] set to "new" to render the
* controller in a dialog * controller in a dialog
* @param {Object} options @see doAction for details * @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) { _executeAction: function (action, options) {
var self = this; var self = this;
@@ -338,7 +338,7 @@ var ActionManager = Widget.extend({
return action; return action;
}) })
.fail(function () { .guardedCatch(function () {
self._removeAction(action.jsID); self._removeAction(action.jsID);
}); });
}, },
@@ -349,7 +349,7 @@ var ActionManager = Widget.extend({
* @private * @private
* @param {Object} action * @param {Object} action
* @param {Object} options @see doAction for details * @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 * dialog appended to the DOM
*/ */
_executeActionInDialog: function (action, options) { _executeActionInDialog: function (action, options) {
@@ -401,7 +401,7 @@ var ActionManager = Widget.extend({
return action; return action;
}); });
}).fail(function () { }).guardedCatch(function () {
self._removeAction(action.jsID); self._removeAction(action.jsID);
}); });
}, },
@@ -412,14 +412,14 @@ var ActionManager = Widget.extend({
* @param {Object} action the description of the action to execute * @param {Object} action the description of the action to execute
* @param {string} action.tag the key of the action in the action_registry * @param {string} action.tag the key of the action in the action_registry
* @param {Object} options @see doAction for details * @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) { _executeClientAction: function (action, options) {
var self = this; var self = this;
var ClientAction = core.action_registry.get(action.tag); var ClientAction = core.action_registry.get(action.tag);
if (!ClientAction) { if (!ClientAction) {
console.error("Could not find client action " + action.tag, action); console.error("Could not find client action " + action.tag, action);
return $.Deferred().reject(); return Promise.reject();
} }
if (!(ClientAction.prototype instanceof AbstractAction)) { if (!(ClientAction.prototype instanceof AbstractAction)) {
console.warn('The client action ' + action.tag + ' should be an instance of AbstractAction!'); console.warn('The client action ' + action.tag + ' should be an instance of AbstractAction!');
@@ -431,7 +431,7 @@ var ActionManager = Widget.extend({
if (next) { if (next) {
return this.doAction(next, options); return this.doAction(next, options);
} }
return $.when(); return Promise.resolve();
} }
var controllerID = _.uniqueId('controller_'); var controllerID = _.uniqueId('controller_');
@@ -449,12 +449,14 @@ var ActionManager = Widget.extend({
}; };
this.controllers[controllerID] = controller; this.controllers[controllerID] = controller;
action.controllerID = controllerID; 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 // AAB: this should be done automatically in AbstractAction, so that
// it can be overriden by actions that have specific stuff to push // it can be overriden by actions that have specific stuff to push
// (e.g. Discuss, Views) // (e.g. Discuss, Views)
self._pushState(controllerID, {}); self._pushState(controllerID, {});
}); });
return prom;
}, },
/** /**
* Executes actions of type 'ir.actions.act_window_close', i.e. closes the * 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.effect] effect to show up, e.g. rainbow man.
* @param {Object} [action.infos] infos on performing the close action. * @param {Object} [action.infos] infos on performing the close action.
* Useful for providing some context for the `on_close` handler. * Useful for providing some context for the `on_close` handler.
* @returns {Deferred} resolved immediately * @returns {Promise} resolved immediately
*/ */
_executeCloseAction: function (action, options) { _executeCloseAction: function (action, options) {
var result; var result;
@@ -484,7 +486,7 @@ var ActionManager = Widget.extend({
this.trigger_up('show_effect', action.effect); this.trigger_up('show_effect', action.effect);
} }
return $.when(result); return Promise.resolve(result);
}, },
/** /**
* Executes actions of type 'ir.actions.server'. * 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 {integer} action.id the db ID of the action to execute
* @param {Object} [action.context] * @param {Object} [action.context]
* @param {Object} options @see doAction for details * @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) { _executeServerAction: function (action, options) {
var self = this; var self = this;
@@ -520,7 +522,7 @@ var ActionManager = Widget.extend({
* @param {string} [action.target] set to 'self' to redirect in the current page, * @param {string} [action.target] set to 'self' to redirect in the current page,
* redirects to a new page by default * redirects to a new page by default
* @param {Object} options @see doAction for details * @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) * when redirecting to a new page)
*/ */
_executeURLAction: function (action, options) { _executeURLAction: function (action, options) {
@@ -531,7 +533,7 @@ var ActionManager = Widget.extend({
if (action.target === 'self') { if (action.target === 'self') {
framework.redirect(url); framework.redirect(url);
return $.Deferred(); return Promise.resolve();
} else { } else {
var w = window.open(url, '_blank'); var w = window.open(url, '_blank');
if (!w || w.closed || typeof w.closed === 'undefined') { if (!w || w.closed || typeof w.closed === 'undefined') {
@@ -544,7 +546,7 @@ var ActionManager = Widget.extend({
options.on_close(); options.on_close();
return $.when(); return Promise.resolve();
}, },
/** /**
* Returns a description of the controllers in the given controller stack. * Returns a description of the controllers in the given controller stack.
@@ -653,14 +655,14 @@ var ActionManager = Widget.extend({
* @param {Object} action * @param {Object} action
* @param {string} action.type * @param {string} action.type
* @param {Object} options * @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 * if the type of action isn't supported, or if the action can't be
* executed * executed
*/ */
_handleAction: function (action, options) { _handleAction: function (action, options) {
if (!action.type) { if (!action.type) {
console.error("No type for action", action); console.error("No type for action", action);
return $.Deferred().reject(); return Promise.reject();
} }
switch (action.type) { switch (action.type) {
case 'ir.actions.act_url': case 'ir.actions.act_url':
@@ -674,7 +676,7 @@ var ActionManager = Widget.extend({
default: default:
console.error("The ActionManager can't handle actions of type " + console.error("The ActionManager can't handle actions of type " +
action.type, action); action.type, action);
return $.Deferred().reject(); return Promise.reject();
} }
}, },
/** /**
@@ -749,16 +751,17 @@ var ActionManager = Widget.extend({
* @private * @private
* @param {integer|string} action's ID or xml ID * @param {integer|string} action's ID or xml ID
* @param {Object} context * @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) { _loadAction: function (actionID, context) {
var def = $.Deferred(); var self = this;
this.trigger_up('load_action', { return new Promise(function (resolve, reject) {
actionID: actionID, self.trigger_up('load_action', {
context: context, actionID: actionID,
on_success: def.resolve.bind(def), context: context,
on_success: resolve,
});
}); });
return def;
}, },
/** /**
* Preprocesses the action before it is handled by the ActionManager * Preprocesses the action before it is handled by the ActionManager
@@ -815,20 +818,20 @@ var ActionManager = Widget.extend({
* *
* @private * @private
* @param {string} controllerID * @param {string} controllerID
* @returns {Deferred} resolved when the controller has been restored * @returns {Promise} resolved when the controller has been restored
*/ */
_restoreController: function (controllerID) { _restoreController: function (controllerID) {
var self = this; var self = this;
var controller = this.controllers[controllerID]; var controller = this.controllers[controllerID];
// AAB: AbstractAction should define a proper hook to execute code when // 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 action = this.actions[controller.actionID];
var def; var def;
if (action.on_reverse_breadcrumb) { if (action.on_reverse_breadcrumb) {
def = action.on_reverse_breadcrumb(); def = action.on_reverse_breadcrumb();
} }
return $.when(def).then(function () { return Promise.resolve(def).then(function () {
return $.when(controller.widget.do_show()).then(function () { return Promise.resolve(controller.widget.do_show()).then(function () {
var index = _.indexOf(self.controllerStack, controllerID); var index = _.indexOf(self.controllerStack, controllerID);
self._pushController(controller, index); self._pushController(controller, index);
}); });
@@ -844,7 +847,7 @@ var ActionManager = Widget.extend({
* *
* @private * @private
* @param {Object} controller * @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) { _startController: function (controller) {
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
@@ -144,7 +144,7 @@ ActionManager.include({
* @param {integer} [options.index=0] the controller's index in the stack * @param {integer} [options.index=0] the controller's index in the stack
* @param {boolean} [options.lazy=false] set to true to differ the * @param {boolean} [options.lazy=false] set to true to differ the
* initialization of the controller's widget * 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) { _createViewController: function (action, viewType, viewOptions, options) {
var self = this; var self = this;
@@ -153,7 +153,7 @@ ActionManager.include({
// the requested view type isn't specified in the action (e.g. // 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 // action with list view only, user clicks on a row in the list, it
// tries to switch to form view) // tries to switch to form view)
return $.Deferred().reject(); return Promise.reject();
} }
options = options || {}; options = options || {};
@@ -186,26 +186,31 @@ ActionManager.include({
// communication with trigger_up // communication with trigger_up
controllerID: controllerID, controllerID: controllerID,
}); });
var rejection;
var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions); var view = new viewDescr.Widget(viewDescr.fieldsView, viewOptions);
var def = $.Deferred(); var def = new Promise(function (resolve, reject) {
action.controllers[viewType] = def; rejection = reject;
view.getController(this).then(function (widget) { view.getController(self).then(function (widget) {
if (def.state() === 'rejected') { if (def.rejected) {
// the deferred has been rejected meanwhile, meaning that // the promise has been rejected meanwhile, meaning that
// the action has been removed, so simply destroy the widget // the action has been removed, so simply destroy the widget
widget.destroy(); widget.destroy();
} else { } else {
controller.widget = widget; controller.widget = widget;
def.resolve(controller); resolve(controller);
} }
}).fail(def.reject.bind(def)); }).guardedCatch(reject);
def.fail(function () { });
// Need to define an reject property to call it into _destroyWindowAction
def.reject = rejection;
def.guardedCatch(function () {
def.rejected = true;
delete self.controllers[controllerID]; delete self.controllers[controllerID];
}); });
action.controllers[viewType] = def;
} else { } else {
action.controllers[viewType] = $.Deferred().resolve(controller); action.controllers[viewType] = Promise.resolve(controller);
} }
return action.controllers[viewType]; return action.controllers[viewType];
}, },
/** /**
@@ -217,18 +222,22 @@ ActionManager.include({
*/ */
_destroyWindowAction: function (action) { _destroyWindowAction: function (action) {
var self = this; var self = this;
_.each(action.controllers, function (controllerDef) { for (var c in action.controllers) {
var controllerDef = action.controllers[c];
controllerDef.then(function (controller) { controllerDef.then(function (controller) {
delete self.controllers[controller.jsID]; delete self.controllers[controller.jsID];
if (controller.widget) { if (controller.widget) {
controller.widget.destroy(); controller.widget.destroy();
} }
}); });
// reject the deferred if it is not yet resolved, so that the // If controllerDef is not resolved yet, reject it so that the
// controller is correctly destroyed as soon as it is ready, and // controller will be correctly destroyed as soon as it'll be ready,
// its reference is removed // and its reference will be removed. Lazy-loaded controllers do
controllerDef.reject(); // not have a reject function on their promise
}); if (controllerDef.reject) {
controllerDef.reject();
}
}
}, },
/** /**
* Executes actions of type 'ir.actions.act_window'. * Executes actions of type 'ir.actions.act_window'.
@@ -239,13 +248,13 @@ ActionManager.include({
* @param {Object} options @see doAction for details * @param {Object} options @see doAction for details
* @param {integer} [options.resID] the current res ID * @param {integer} [options.resID] the current res ID
* @param {string} [options.viewType] the view to open * @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) { _executeWindowAction: function (action, options) {
var self = this; var self = this;
return this.dp.add(this._loadViews(action)).then(function (fieldsViews) { return this.dp.add(this._loadViews(action)).then(function (fieldsViews) {
var views = self._generateActionViews(action, 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.views = views;
action.controlPanelFieldsView = fieldsViews.search; action.controlPanelFieldsView = fieldsViews.search;
action.controllers = {}; action.controllers = {};
@@ -285,7 +294,7 @@ ActionManager.include({
self.controllerStack.push(lazyLoadedController.jsID); self.controllerStack.push(lazyLoadedController.jsID);
}); });
} }
return self.dp.add($.when(lazyViewDef)) return self.dp.add(Promise.resolve(lazyViewDef))
.then(function () { .then(function () {
var viewOptions = { var viewOptions = {
controllerState: options.controllerState, controllerState: options.controllerState,
@@ -300,7 +309,7 @@ ActionManager.include({
action.controllerID = controller.jsID; action.controllerID = controller.jsID;
return self._executeAction(action, options); return self._executeAction(action, options);
}) })
.fail(self._destroyWindowAction.bind(self, action)); .guardedCatch(self._destroyWindowAction.bind(self, action));
}); });
}, },
/** /**
@@ -389,7 +398,7 @@ ActionManager.include({
* *
* @private * @private
* @param {Object} action * @param {Object} action
* @returns {Deferred} * @returns {Promise}
*/ */
_loadViews: function (action) { _loadViews: function (action) {
var inDialog = action.target === 'new'; var inDialog = action.target === 'new';
@@ -443,11 +452,11 @@ ActionManager.include({
return this.clearUncommittedChanges().then(function () { return this.clearUncommittedChanges().then(function () {
// AAB: this will be done directly in AbstractAction's restore // AAB: this will be done directly in AbstractAction's restore
// function // function
var def; var def = Promise.resolve();
if (action.on_reverse_breadcrumb) { if (action.on_reverse_breadcrumb) {
def = 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); return self._switchController(action, controller.viewType);
}); });
}); });
@@ -461,14 +470,14 @@ ActionManager.include({
* @private * @private
* @param {Object} controller the controller to switch to * @param {Object} controller the controller to switch to
* @param {Object} [viewOptions] * @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) { _switchController: function (action, viewType, viewOptions) {
var self = this; var self = this;
var view = _.findWhere(action.views, {type: viewType}); var view = _.findWhere(action.views, {type: viewType});
if (!view) { if (!view) {
// can't switch to an unknown view // can't switch to an unknown view
return $.Deferred().reject(); return Promise.reject();
} }
var currentController = this.getCurrentController(); var currentController = this.getCurrentController();
var index; var index;
@@ -519,28 +528,30 @@ ActionManager.include({
}; };
var controllerDef = action.controllers[viewType]; var controllerDef = action.controllers[viewType];
if (!controllerDef || controllerDef.state() === 'rejected') { if (controllerDef) {
// 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 {
controllerDef = controllerDef.then(function (controller) { controllerDef = controllerDef.then(function (controller) {
if (!controller.widget) { if (!controller.widget) {
// lazy loaded -> load it now (with same jsID) // lazy loaded -> load it now (with same jsID)
return newController(controller.jsID); return newController(controller.jsID);
} else { } 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.widget.reload(viewOptions).then(function () {
return controller; 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) { return this.dp.add(controllerDef).then(function (controller) {
@@ -577,11 +588,11 @@ ActionManager.include({
var env = ev.data.env; var env = ev.data.env;
var context = new Context(env.context, actionData.context || {}); var context = new Context(env.context, actionData.context || {});
var recordID = env.currentID || null; // pyUtils handles null value, not undefined 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 // determine the action to execute according to the actionData
if (actionData.special) { 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') { } else if (actionData.type === 'object') {
// call a Python Object method, which may return an action to execute // call a Python Object method, which may return an action to execute
var args = recordID ? [[recordID]] : [env.resIDs]; var args = recordID ? [[recordID]] : [env.resIDs];
@@ -611,13 +622,15 @@ ActionManager.include({
active_ids: env.resIDs, active_ids: env.resIDs,
active_id: recordID, active_id: recordID,
})); }));
} else {
def = new Promise(function () {});
} }
// use the DropPrevious to prevent from executing the handler if another // use the DropPrevious to prevent from executing the handler if another
// request (doAction, switchView...) has been done meanwhile ; execute // request (doAction, switchView...) has been done meanwhile ; execute
// the fail handler if the 'call_button' or 'loadAction' failed but not // the fail handler if the 'call_button' or 'loadAction' failed but not
// if the request failed due to the DropPrevious, // 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) { this.dp.add(def).then(function (action) {
// show effect if button have effect attribute // show effect if button have effect attribute
// rainbowman can be displayed from two places: from attribute on a button or from python // rainbowman can be displayed from two places: from attribute on a button or from python
@@ -40,35 +40,36 @@ ActionManager.include({
* report generation and download. * report generation and download.
* *
* @param {string} url * @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 * rejected if something went wrong during the report generation
*/ */
_downloadReport: function (url) { _downloadReport: function (url) {
var self = this;
framework.blockUI(); framework.blockUI();
var def = $.Deferred(); return new Promise(function (resolve, reject) {
var type = 'qweb-' + url.split('/')[2]; var type = 'qweb-' + url.split('/')[2];
var blocked = !session.get_file({ var blocked = !session.get_file({
url: '/report/download', url: '/report/download',
data: { data: {
data: JSON.stringify([url, type]), data: JSON.stringify([url, type]),
}, },
success: def.resolve.bind(def), success: resolve,
error: function () { error: function () {
crash_manager.rpc_error.apply(crash_manager, arguments); crash_manager.rpc_error.apply(crash_manager, arguments);
def.reject(); reject();
}, },
complete: framework.unblockUI, 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 * @private
* @param {Object} action the description of the action to execute * @param {Object} action the description of the action to execute
* @param {Object} options @see doAction for details * @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){ _triggerDownload: function (action, options, type){
var self = this; var self = this;
@@ -97,7 +98,7 @@ ActionManager.include({
* @private * @private
* @param {Object} action the description of the action to execute * @param {Object} action the description of the action to execute
* @param {Object} options @see doAction for details * @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) { _executeReportAction: function (action, options) {
var self = this; var self = this;
@@ -125,7 +126,7 @@ ActionManager.include({
} else { } else {
console.error("The ActionManager can't handle reports of type " + console.error("The ActionManager can't handle reports of type " +
action.report_type, action); action.report_type, action);
return $.Deferred().reject(); return Promise.reject();
} }
}, },
/** /**
@@ -135,7 +136,7 @@ ActionManager.include({
* *
* @param {Object} action * @param {Object} action
* @param {Object} options * @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) { _executeReportClientAction: function (action, options) {
var urls = this._makeReportUrls(action); var urls = this._makeReportUrls(action);
@@ -75,7 +75,7 @@ var ActionMixin = {
* Called by the action manager when action is restored (typically, when the * Called by the action manager when action is restored (typically, when the
* user clicks on the action in the breadcrumb) * user clicks on the action in the breadcrumb)
* *
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
willRestore: function () {}, willRestore: function () {},
@@ -89,14 +89,14 @@ var ActionMixin = {
* example, if the user has edited a form, maybe we should ask him if we * 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 * can discard all his changes when we switch to another action. In that
* case, the action manager will call this method. If the returned * 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. * 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 * otherwise
*/ */
canBeRemoved: function () { canBeRemoved: function () {
return $.when(); return Promise.resolve();
}, },
/** /**
* This function is called when the current state of the action * This function is called when the current state of the action
@@ -26,6 +26,8 @@ var Loading = Widget.extend({
this._super(parent); this._super(parent);
this.count = 0; this.count = 0;
this.blocked_ui = false; this.blocked_ui = false;
},
start: function() {
core.bus.on('rpc_request', this, this.request_call); core.bus.on('rpc_request', this, this.request_call);
core.bus.on("rpc_response", this, this.response_call); core.bus.on("rpc_response", this, this.response_call);
core.bus.on("rpc_response_failed", this, this.response_call); core.bus.on("rpc_response_failed", this, this.response_call);
+7 -5
View File
@@ -64,20 +64,22 @@ var Menu = Widget.extend({
// Apps Menu // Apps Menu
this._appsMenu = new AppsMenu(self, this.menu_data); this._appsMenu = new AppsMenu(self, this.menu_data);
this._appsMenu.appendTo(this.$menu_apps); var appsMenuProm = this._appsMenu.appendTo(this.$menu_apps);
// Systray Menu // Systray Menu
this.systray_menu = new SystrayMenu(this); this.systray_menu = new SystrayMenu(this);
this.systray_menu.attachTo(this.$('.o_menu_systray')); var systrayMenuProm = this.systray_menu.attachTo(this.$('.o_menu_systray')).then(function() {
dom.initAutoMoreMenu(self.$section_placeholder, {
dom.initAutoMoreMenu(this.$section_placeholder, {
maxWidth: function () { maxWidth: function () {
return self.$el.width() - (self.$menu_apps.outerWidth(true) + self.$menu_brand_placeholder.outerWidth(true) + self.systray_menu.$el.outerWidth(true)); return self.$el.width() - (self.$menu_apps.outerWidth(true) + self.$menu_brand_placeholder.outerWidth(true) + self.systray_menu.$el.outerWidth(true));
}, },
sizeClass: 'SM', 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) { change_menu_section: function (primary_menu_id) {
if (!this.$menu_sections[primary_menu_id]) { if (!this.$menu_sections[primary_menu_id]) {
+3 -4
View File
@@ -28,7 +28,7 @@ var Pager = Widget.extend({
* @param {boolean} [options.can_edit] editable feature of the pager * @param {boolean} [options.can_edit] editable feature of the pager
* @param {boolean} [options.single_page_hidden] (not) to display the pager * @param {boolean} [options.single_page_hidden] (not) to display the pager
* if only one page * if only one page
* @param {function} [options.validate] callback returning a Deferred to * @param {function} [options.validate] callback returning a Promise to
* validate changes * validate changes
*/ */
init: function (parent, size, current_min, limit, options) { init: function (parent, size, current_min, limit, options) {
@@ -46,7 +46,7 @@ var Pager = Widget.extend({
can_edit: true, // editable can_edit: true, // editable
single_page_hidden: false, // displayed even if there is a single page single_page_hidden: false, // displayed even if there is a single page
validate: function() { validate: function() {
return $.Deferred().resolve(); return Promise.resolve();
}, },
withAccessKey: true, // can be disabled, for example, for x2m widgets withAccessKey: true, // can be disabled, for example, for x2m widgets
}); });
@@ -55,7 +55,7 @@ var Pager = Widget.extend({
/** /**
* Renders the pager * Renders the pager
* *
* @returns {jQuery.Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this.$value = this.$('.o_pager_value'); this.$value = this.$('.o_pager_value');
@@ -216,7 +216,6 @@ var Pager = Widget.extend({
} }
self.trigger('pager_changed', _.clone(self.state)); self.trigger('pager_changed', _.clone(self.state));
} }
}).always(function() {
// Render the pager's new state (removes the input) // Render the pager's new state (removes the input)
self._render(); self._render();
}); });
+1 -1
View File
@@ -153,7 +153,7 @@ var Sidebar = Widget.extend({
action_id: item.action.id, action_id: item.action.id,
context: context, context: context,
}, },
}).done(function (result) { }).then(function (result) {
result.context = new Context( result.context = new Context(
result.context || {}, activeIdsContext) result.context || {}, activeIdsContext)
.set_eval_context(context); .set_eval_context(context);
@@ -16,20 +16,19 @@ var SystrayMenu = Widget.extend({
this._super(parent); this._super(parent);
this.items = []; this.items = [];
this.widgets = []; this.widgets = [];
this.load = $.Deferred();
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
var self = this; var self = this;
self._super.apply(this, arguments); self._super.apply(this, arguments);
self._loadItems(); 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;
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
+4 -4
View File
@@ -22,7 +22,7 @@ var UserMenu = Widget.extend({
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
var self = this; var self = this;
@@ -36,7 +36,7 @@ var UserMenu = Widget.extend({
var $avatar = self.$('.oe_topbar_avatar'); var $avatar = self.$('.oe_topbar_avatar');
if (!session.uid) { if (!session.uid) {
$avatar.attr('src', $avatar.data('default-src')); $avatar.attr('src', $avatar.data('default-src'));
return $.when(); return Promise.resolve();
} }
var topbar_name = session.name; var topbar_name = session.name;
if (session.debug) { if (session.debug) {
@@ -67,7 +67,7 @@ var UserMenu = Widget.extend({
.then(function (url) { .then(function (url) {
framework.redirect(url); framework.redirect(url);
}) })
.fail(function (result, ev){ .guardedCatch(function (result, ev){
ev.preventDefault(); ev.preventDefault();
framework.redirect('https://accounts.odoo.com/account'); framework.redirect('https://accounts.odoo.com/account');
}); });
@@ -100,7 +100,7 @@ var UserMenu = Widget.extend({
model: "res.users", model: "res.users",
method: "action_get" method: "action_get"
}) })
.done(function (result) { .then(function (result) {
result.res_id = session.uid; result.res_id = session.uid;
self.do_action(result); self.do_action(result);
}); });
+34 -31
View File
@@ -111,7 +111,7 @@ return AbstractWebClient.extend({
instanciate_menu_widgets: function () { instanciate_menu_widgets: function () {
var self = this; var self = this;
var defs = []; var proms = [];
return this.load_menus().then(function (menuData) { return this.load_menus().then(function (menuData) {
self.menu_data = menuData; self.menu_data = menuData;
@@ -122,8 +122,8 @@ return AbstractWebClient.extend({
self.menu.destroy(); self.menu.destroy();
} }
self.menu = new Menu(self, menuData); self.menu = new Menu(self, menuData);
defs.push(self.menu.prependTo(self.$el)); proms.push(self.menu.prependTo(self.$el));
return $.when.apply($, defs); return Promise.all(proms);
}); });
}, },
@@ -133,7 +133,7 @@ return AbstractWebClient.extend({
on_hashchange: function (event) { on_hashchange: function (event) {
if (this._ignore_hashchange) { if (this._ignore_hashchange) {
this._ignore_hashchange = false; this._ignore_hashchange = false;
return $.when(); return Promise.resolve();
} }
var self = this; var self = this;
@@ -181,48 +181,51 @@ return AbstractWebClient.extend({
return this.menu_dm.add(data_manager.load_action(ev.data.action_id)) return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
.then(function (result) { .then(function (result) {
return self.action_mutex.exec(function () { return self.action_mutex.exec(function () {
var completed = $.Deferred(); var completed = new Promise(function (resolve, reject) {
var options = _.extend({}, ev.data.options, { var options = _.extend({}, ev.data.options, {
clear_breadcrumbs: true, clear_breadcrumbs: true,
action_menu_id: ev.data.menu_id, 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);
}); });
$.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));
});
setTimeout(function () {
completed.resolve();
}, 2000);
return completed; return completed;
}); });
}); });
}, },
_on_app_clicked_done: function (ev) { _on_app_clicked_done: function (ev) {
core.bus.trigger('change_menu_section', ev.data.menu_id); core.bus.trigger('change_menu_section', ev.data.menu_id);
return $.Deferred().resolve(); return Promise.resolve();
}, },
on_menu_clicked: function (ev) { on_menu_clicked: function (ev) {
var self = this; var self = this;
return this.menu_dm.add(data_manager.load_action(ev.data.action_id)) return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
.then(function (result) { .then(function (result) {
self.$el.removeClass('o_mobile_menu_opened');
return self.action_mutex.exec(function () { return self.action_mutex.exec(function () {
var completed = $.Deferred(); var completed = new Promise(function (resolve, reject) {
$.when(self._openMenu(result, { Promise.resolve(self._openMenu(result, {
clear_breadcrumbs: true, clear_breadcrumbs: true,
})).always(function () { })).then(resolve).guardedCatch(reject);
completed.resolve();
setTimeout(function () {
resolve();
}, 2000);
}); });
setTimeout(function () {
completed.resolve();
}, 2000);
return completed; return completed;
}); });
}).always(function () { }).guardedCatch(function () {
self.$el.removeClass('o_mobile_menu_opened'); self.$el.removeClass('o_mobile_menu_opened');
}); });
}, },
@@ -233,7 +236,7 @@ return AbstractWebClient.extend({
* @private * @private
* @param {Object} action * @param {Object} action
* @param {Object} options * @param {Object} options
* @returns {Deferred} * @returns {Promise}
*/ */
_openMenu: function (action, options) { _openMenu: function (action, options) {
return this.do_action(action, options); return this.do_action(action, options);
+165 -180
View File
@@ -3,22 +3,19 @@ odoo.define('web.ajax', function (require) {
var config = require('web.config'); var config = require('web.config');
var core = require('web.core'); var core = require('web.core');
var utils = require('web.utils');
var time = require('web.time'); var time = require('web.time');
var download = require('web.download'); var download = require('web.download');
var contentdisposition = require('web.contentdisposition'); var contentdisposition = require('web.contentdisposition');
var _t = core._t; var _t = core._t;
function genericJsonRpc (fct_name, params, settings, fct) { function _genericJsonRpc (fct_name, params, settings, fct) {
var shadow = settings.shadow || false; var shadow = settings.shadow || false;
delete settings.shadow; delete settings.shadow;
if (!shadow) { if (!shadow) {
core.bus.trigger('rpc_request'); core.bus.trigger('rpc_request');
} }
var deferred = $.Deferred();
var data = { var data = {
jsonrpc: "2.0", jsonrpc: "2.0",
method: fct_name, method: fct_name,
@@ -26,7 +23,7 @@ function genericJsonRpc (fct_name, params, settings, fct) {
id: Math.floor(Math.random() * 1000 * 1000 * 1000) id: Math.floor(Math.random() * 1000 * 1000 * 1000)
}; };
var xhr = fct(data); var xhr = fct(data);
var result = xhr.pipe(function(result) { var result = xhr.then(function(result) {
core.bus.trigger('rpc:result', data, result); core.bus.trigger('rpc:result', data, result);
if (result.error !== undefined) { if (result.error !== undefined) {
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { 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)); console.error("Server application error", JSON.stringify(result.error));
} }
} }
return $.Deferred().reject("server", result.error); return Promise.reject({type: "server", error: result.error});
} else { } else {
return result.result; return result.result;
} }
}, function() { }, function() {
//console.error("JsonRPC communication error", _.toArray(arguments)); //console.error("JsonRPC communication error", _.toArray(arguments));
var def = $.Deferred(); var reason = {
return def.reject.apply(def, ["communication"].concat(_.toArray(arguments))); 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')); var rejection;
var promise = new Promise(function (resolve, reject) {
rejection = reject;
result.then(function (result) {
if (!shadow) {
core.bus.trigger('rpc_response');
}
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()});
}
});
});
// FIXME: jsonp?
promise.abort = function () {
rejection({
message: "XmlHttpRequestError abort",
event: $.Event('abort')
});
if (xhr.abort) { if (xhr.abort) {
xhr.abort(); xhr.abort();
} }
}; };
promise.guardedCatch(function (reason) { // Allow promise user to disable rpc_error call in case of failure
result.then(function (result) { setTimeout(function () {
if (!shadow) { // we want to execute this handler after all others (hence
core.bus.trigger('rpc_response'); // setTimeout) to let the other handlers prevent the event
} if (!reason.event.isDefaultPrevented()) {
deferred.resolve(result); core.bus.trigger('rpc_error', reason.message, reason.event);
}, function (type, error, textStatus, errorThrown) {
if (type === "server") {
if (!shadow) {
core.bus.trigger('rpc_response');
} }
if (error.code === 100) { }, 0);
core.bus.trigger('invalidate_session');
}
deferred.reject(error, $.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]
},
};
deferred.reject(nerror, $.Event());
}
}); });
deferred.fail(function () { // Allow deferred user to disable rpc_error call in fail return promise;
deferred.fail(function (error, event) { };
if (!event.isDefaultPrevented()) {
core.bus.trigger('rpc_error', error, event);
}
});
});
return deferred;
}
function jsonRpc(url, fct_name, params, settings) { function jsonRpc(url, fct_name, params, settings) {
settings = settings || {}; settings = settings || {};
return genericJsonRpc(fct_name, params, settings, function(data) { return _genericJsonRpc(fct_name, params, settings, function(data) {
return $.ajax(url, _.extend({}, settings, { return $.ajax(url, _.extend({}, settings, {
url: url, url: url,
dataType: 'json', dataType: 'json',
@@ -110,19 +128,6 @@ function rpc(url, params, settings) {
return jsonRpc(url, 'call', 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 * Load css asynchronously: fetch it from the url parameter and add a link tag
@@ -131,26 +136,27 @@ function realSetTimeout (fct, millis) {
* immediately. * immediately.
* *
* @param {String} url of the css to be fetched * @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 loadCSS = (function () {
var urlDefs = Object.create(null); var urlDefs = {};
return function loadCSS(url) { return function loadCSS(url) {
if (url in urlDefs) { if (url in urlDefs) {
// nothing to do here // nothing to do here
} else if ($('link[href="' + url + '"]').length) { } else if ($('link[href="' + url + '"]').length) {
// the link is already in the DOM, the deferred can be resolved // the link is already in the DOM, the promise can be resolved
urlDefs[url] = $.when(); urlDefs[url] = Promise.resolve();
} else { } else {
var $link = $('<link>', { var $link = $('<link>', {
'href': url, 'href': url,
'rel': 'stylesheet', 'rel': 'stylesheet',
'type': 'text/css' 'type': 'text/css'
}); });
urlDefs[url] = $.Deferred(); urlDefs[url] = new Promise(function (resolve, reject) {
$link.on('load', function () { $link.on('load', function () {
urlDefs[url].resolve(); resolve();
});
}); });
$('head').append($link); $('head').append($link);
} }
@@ -159,44 +165,45 @@ var loadCSS = (function () {
})(); })();
var loadJS = (function () { var loadJS = (function () {
var urls = []; var dependenciesPromise = {};
var defs = [];
var load = function loadJS(url) { var load = function loadJS(url) {
// Check the DOM to see if a script with the specified url is already there // Check the DOM to see if a script with the specified url is already there
var alreadyRequired = ($('script[src="' + url + '"]').length > 0); var alreadyRequired = ($('script[src="' + url + '"]').length > 0);
// If loadJS was already called with the same URL, it will have a registered deferred indicating if // 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 deferred has to be initialized. This is initialized // the script has been fully loaded. If not, the promise has to be initialized.
// as already resolved if the script was already there without the need of loadJS. // This is initialized as already resolved if the script was already there without the need of loadJS.
var index = _.indexOf(urls, url); if (url in dependenciesPromise) {
if (index < 0) { return dependenciesPromise[url];
urls.push(url);
index = defs.push(alreadyRequired ? $.when() : $.Deferred()) - 1;
} }
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 dependenciesPromise[url] = scriptLoadedPromise;
// deferred is marked to be resolved on script load and rejected on script error. return scriptLoadedPromise;
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;
}; };
return load; return load;
@@ -308,22 +315,6 @@ function get_file(options) {
} }
function post (controller_url, data) { 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(); var postData = new FormData();
$.each(data, function(i,val) { $.each(data, function(i,val) {
@@ -333,25 +324,20 @@ function post (controller_url, data) {
postData.append('csrf_token', core.csrf_token); postData.append('csrf_token', core.csrf_token);
} }
var xhr = new XMLHttpRequest(); return new Promise(function (resolve, reject) {
if(xhr.upload) xhr.upload.addEventListener('progress', progressHandler(Def), false); $.ajax(controller_url, {
data: postData,
var ajaxDef = $.ajax(controller_url, { processData: false,
xhr: function() {return xhr;}, contentType: false,
data: postData, type: 'POST'
processData: false, }).then(resolve).fail(reject);
contentType: false, });
type: 'POST'
}).then(function (data) {Def.resolve(data);})
.fail(function (data) {Def.reject(data);});
return Def;
} }
/** /**
* Loads an XML file according to the given URL and adds its associated qweb * 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 * 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 * 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 * 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 {string} [url] - an URL where to find qweb templates
* @param {QWeb} [qweb] - the engine to which the templates need to be added * @param {QWeb} [qweb] - the engine to which the templates need to be added
* @returns {Deferred} * @returns {Promise}
* If no argument is given to the function, the deferred's state * If no argument is given to the function, the promise's state
* indicates if "all the calls" are finished (see main description). * indicates if "all the calls" are finished (see main description).
* Otherwise, it indicates when the templates associated to the given * Otherwise, it indicates when the templates associated to the given
* url have been loaded. * url have been loaded.
@@ -369,63 +355,64 @@ var loadXML = (function () {
// Some "static" variables associated to the loadXML function // Some "static" variables associated to the loadXML function
var isLoading = false; var isLoading = false;
var loadingsData = []; var loadingsData = [];
var allLoadingsDef = $.when();
var seenURLs = []; var seenURLs = [];
return function (url, qweb) { return function (url, qweb) {
// If no argument, simply returns the deferred which indicates when
// "all the calls" are finished
if (!url || !qweb) {
return allLoadingsDef;
}
// If the given URL has already been seen, do nothing but returning the
// associated deferred
if (_.contains(seenURLs, url)) {
var oldLoadingData = _.findWhere(loadingsData, {url: url});
return oldLoadingData ? oldLoadingData.def : $.when();
}
seenURLs.push(url);
// Add the information about the new data to load: the url, the qweb
// engine and the associated deferred
var newLoadingData = {
url: url,
qweb: qweb,
def: $.Deferred(),
};
loadingsData.push(newLoadingData);
// If not already started, start the loading loop (reinitialize the
// "all the calls" deferred to an unresolved state)
if (!isLoading) {
allLoadingsDef = $.Deferred();
_load();
}
// Return the deferred associated to the new given URL
return newLoadingData.def;
function _load() { function _load() {
isLoading = true; isLoading = true;
if (loadingsData.length) { if (loadingsData.length) {
// There is something to load, load it, resolve the associated // There is something to load, load it, resolve the associated
// deferred then start loading the next one // promise then start loading the next one
var loadingData = loadingsData[0]; var loadingData = loadingsData[0];
loadingData.qweb.add_template(loadingData.url, function () { loadingData.qweb.add_template(loadingData.url, function () {
// Remove from array only now so that multiple calls to // Remove from array only now so that multiple calls to
// loadXML with the same URL returns the right deferred // loadXML with the same URL returns the right promise
loadingsData.shift(); loadingsData.shift();
loadingData.def.resolve(); loadingData.resolve();
_load(); _load();
}); });
} else { } else {
// There is nothing to load anymore, so resolve the // There is nothing to load anymore, so resolve the
// "all the calls" deferred // "all the calls" promise
isLoading = false; isLoading = false;
allLoadingsDef.resolve();
} }
} }
// If no argument, simply returns the promise which indicates when
// "all the calls" are finished
if (!url || !qweb) {
return Promise.resolve();
}
// If the given URL has already been seen, do nothing but returning the
// associated promise
if (_.contains(seenURLs, url)) {
var oldLoadingData = _.findWhere(loadingsData, {url: url});
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 promise
var newLoadingData = {
url: url,
qweb: qweb,
};
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" promise to an unresolved state)
if (!isLoading) {
_load();
}
// Return the promise associated to the new given URL
return newLoadingData.def;
}; };
})(); })();
@@ -444,7 +431,7 @@ var loadAsset = (function () {
var load = function loadAsset(xmlId) { var load = function loadAsset(xmlId) {
if (cache[xmlId]) { if (cache[xmlId]) {
return $.when(cache[xmlId]); return Promise.resolve(cache[xmlId]);
} }
var params = { var params = {
args: [xmlId, { args: [xmlId, {
@@ -493,22 +480,20 @@ var loadAsset = (function () {
* @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template * @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template
* contains the script and link to be loaded * contains the script and link to be loaded
* *
* @returns {Deferred} * @returns {Promise}
*/ */
function loadLibs (libs) { function loadLibs (libs) {
var defs = []; var defs = [];
_.each(libs.jsLibs || [], function (urls) { (libs.jsLibs || []).forEach(function (urls) {
defs.push($.when.apply($, defs).then(function () { defs.push(Promise.all(defs).then(function () {
if (typeof(urls) === 'string') { if (typeof(urls) === 'string') {
return ajax.loadJS(urls); return ajax.loadJS(urls);
} else { } else {
return $.when.apply($, _.map(urls, function (url) { return Promise.all(urls.map(ajax.loadJS));
return ajax.loadJS(url);
}));
} }
})); }));
}); });
_.each(libs.cssLibs || [], function (url) { (libs.cssLibs || []).forEach(function (url) {
defs.push(ajax.loadCSS(url)); defs.push(ajax.loadCSS(url));
}); });
_.each(libs.assetLibs || [], function (xmlId) { _.each(libs.assetLibs || [], function (xmlId) {
@@ -516,7 +501,7 @@ function loadLibs (libs) {
return loadLibs(asset); return loadLibs(asset);
})); }));
}); });
return $.when.apply($, defs); return Promise.all(defs);
} }
var ajax = { var ajax = {
+122 -117
View File
@@ -8,7 +8,7 @@ odoo.define('web.concurrency', function (require) {
* everything concurrency related in Odoo. * everything concurrency related in Odoo.
* *
* The basic concurrency primitives in Odoo JS are the callback, and the * 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. * whenever possible. We use the jQuery implementation.
* *
* Those functions are really nothing special, but are simply the result of how * Those functions are really nothing special, but are simply the result of how
@@ -19,54 +19,15 @@ var Class = require('web.Class');
return { return {
/** /**
* The jquery implementation for $.when has a (most of the time) useful * Returns a promise resolved after 'wait' milliseconds
* 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
* *
* @param {int} [wait=0] the delay in ms * @param {int} [wait=0] the delay in ms
* @return {Deferred} * @return {Promise}
*/ */
delay: function (wait) { delay: function (wait) {
var def = $.Deferred(); return new Promise(function (resolve) {
setTimeout(def.resolve, wait); setTimeout(resolve, wait);
return def; });
}, },
/** /**
* The DropMisordered abstraction is useful for situations where you have * The DropMisordered abstraction is useful for situations where you have
@@ -93,27 +54,27 @@ return {
this.failMisordered = failMisordered || false; 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 * @param {Promise} promise to ensure add
* @returns {Deferred} * @returns {Promise}
*/ */
add: function (deferred) { add: function (promise) {
var res = $.Deferred(); var self = this;
var seq = this.lsn++;
var self = this, seq = this.lsn++; var res = new Promise(function (resolve, reject) {
deferred.done(function () { promise.then(function (result) {
if (seq > self.rsn) { if (seq > self.rsn) {
self.rsn = seq; self.rsn = seq;
res.resolve.apply(res, arguments); resolve(result);
} else if (self.failMisordered) { } else if (self.failMisordered) {
res.reject(); reject();
} }
}).fail(function () { }).guardedCatch(function (result) {
res.reject.apply(res, arguments); reject(result);
});
}); });
return res;
return res.promise();
}, },
}), }),
/** /**
@@ -152,17 +113,26 @@ return {
*/ */
DropPrevious: Class.extend({ 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} * @returns {Promise}
*/ */
add: function (deferred) { add: function (promise) {
if (this.current_def) { this.current_def.reject(); } if (this.currentDef) {
var res = $.Deferred(); this.currentDef.reject();
deferred.then(res.resolve, res.reject); }
this.current_def = res; var rejection;
return res.promise(); 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. * state and cause some corrupted state.
* *
* Imagine that we have a function to fetch some data _load(), which returns * 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:: * looking like this::
* *
* return this._load().then(function (result) { * return this._load().then(function (result) {
@@ -200,37 +170,51 @@ return {
*/ */
Mutex: Class.extend({ Mutex: Class.extend({
init: function () { init: function () {
this.def = $.Deferred().resolve(); this.lock = Promise.resolve();
this.unlockedDef = undefined; this.queueSize = 0;
this.unlockedProm = undefined;
this._unlock = undefined;
}, },
/** /**
* Add a computation to the queue, it will be executed as soon as the * Add a computation to the queue, it will be executed as soon as the
* previous computations are completed. * previous computations are completed.
* *
* @param {function} action a function which may return a deferred * @param {function} action a function which may return a Promise
* @returns {Deferred} * @returns {Promise}
*/ */
exec: function (action) { exec: function (action) {
var self = this; var self = this;
var current = this.def; var currentLock = this.lock;
var next = this.def = $.Deferred(); var result;
this.unlockedDef = this.unlockedDef || $.Deferred(); this.queueSize++;
return current.then(function() { this.unlockedProm = this.unlockedProm || new Promise(function (resolve) {
return $.when(action()).always(function () { self._unlock = resolve;
next.resolve(); });
if (self.def.state() === 'resolved' && self.unlockedDef) { this.lock = new Promise(function (unlockCurrent) {
self.unlockedDef.resolve(); currentLock.then(function () {
self.unlockedDef = undefined; 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 * @returns {Promise} resolved as soon as the Mutex is unlocked
* (directly if it is currently idle) * (directly if it is currently idle)
*/ */
getUnlockedDef: function () { 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 * 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 * 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 * 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({ MutexedDropPrevious: Class.extend({
init: function () { init: function () {
this.currentDef = null;
this.locked = false; this.locked = false;
this.currentProm = null;
this.pendingAction = null; this.pendingAction = null;
this.pendingDef = null; this.pendingProm = null;
}, },
/** /**
* @param {function} action a function which may return a deferred * @param {function} action a function which may return a promise
* @returns {Deferred} * @returns {Promise}
*/ */
exec: function (action) { exec: function (action) {
var self = this; var self = this;
var resolution;
var rejection;
if (this.locked) { if (this.locked) {
this.pendingAction = action; this.pendingAction = action;
var oldPendingDef = this.pendingDef; var oldPendingDef = this.pendingProm;
var pendingDef = this.pendingDef = $.Deferred();
if (oldPendingDef) { this.pendingProm = new Promise(function (resolve, reject) {
oldPendingDef.reject(); resolution = resolve;
} rejection = reject;
this.currentDef.reject(); if (oldPendingDef) {
return pendingDef.promise(); oldPendingDef.reject();
}
self.currentProm.reject();
});
this.pendingProm.resolve = resolution;
this.pendingProm.reject = rejection;
return this.pendingProm;
} else { } else {
this.locked = true; this.locked = true;
this.currentDef = $.Deferred(); this.currentProm = new Promise(function (resolve, reject) {
$.when(action()) resolution = resolve;
.then(this.currentDef.resolve.bind(this.currentDef)) rejection = reject;
.fail(this.currentDef.reject.bind(this.currentDef)) function unlock() {
.always(function () {
self.locked = false; self.locked = false;
if (self.pendingAction) { if (self.pendingAction) {
var action = self.pendingAction; var action = self.pendingAction;
var prom = self.pendingProm;
self.pendingAction = null; self.pendingAction = null;
self.pendingProm = null;
self.exec(action) self.exec(action)
.then(self.pendingDef.resolve.bind(self.pendingDef)) .then(prom.resolve)
.fail(self.pendingDef.reject.bind(self.pendingDef)); .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 * rejected
* *
* @param {Deferred} [target_def] the deferred to potentially reject * @param {Promise} [target_def] the promise to potentially reject
* @param {Deferred} [reference_def] the reference target * @param {Promise} [reference_def] the reference target
* @returns {Deferred} * @returns {Promise}
*/ */
rejectAfter: function (target_def, reference_def) { rejectAfter: function (target_def, reference_def) {
var res = $.Deferred(); return new Promise(function (resolve, reject) {
target_def.then(res.resolve, res.reject); target_def.then(resolve).guardedCatch(reject);
reference_def.always(res.reject); reference_def.then(reject).guardedCatch(reject);
return res.promise(); });
} }
}; };
+6 -4
View File
@@ -52,9 +52,11 @@ var Dialog = Widget.extend({
* (use this for non-editor frontend features) * (use this for non-editor frontend features)
*/ */
init: function (parent, options) { init: function (parent, options) {
var self = this;
this._super(parent); this._super(parent);
this._opened = $.Deferred(); this._opened = new Promise(function (resolve) {
self._openedResolver = resolve;
});
options = _.defaults(options || {}, { options = _.defaults(options || {}, {
title: _t('Odoo'), subtitle: '', title: _t('Odoo'), subtitle: '',
size: 'large', size: 'large',
@@ -140,7 +142,7 @@ var Dialog = Widget.extend({
def = buttonData.click.call(self, e); def = buttonData.click.call(self, e);
} }
if (buttonData.close) { 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) { if (self.technical) {
@@ -185,7 +187,7 @@ var Dialog = Widget.extend({
self.$modal.attr('open', true); self.$modal.attr('open', true);
self.$modal.removeAttr("aria-hidden"); self.$modal.removeAttr("aria-hidden");
self.$modal.modal('show'); self.$modal.modal('show');
self._opened.resolve(); self._openedResolver();
if (options && options.shouldFocusButtons) { if (options && options.shouldFocusButtons) {
self._onFocusControlButton(); self._onFocusControlButton();
} }
+11 -10
View File
@@ -1,11 +1,10 @@
odoo.define('web.dom_ready', function (require) { odoo.define('web.dom_ready', function (require) {
'use strict'; 'use strict';
var def = $.Deferred(); return new Promise(function (resolve, reject) {
$(def.resolve.bind(def)); $(resolve);
return def; });
}); });
//============================================================================== //==============================================================================
odoo.define('web.dom', function (require) { 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 * that on_attach_callback() will be called on each w with arguments args
*/ */
function _notify(content, callbacks) { function _notify(content, callbacks) {
_.each(callbacks, function (c) { callbacks.forEach(function (c) {
if (c.widget && c.widget.on_attach_callback) { if (c.widget && c.widget.on_attach_callback) {
c.widget.on_attach_callback(c.callback_args); c.widget.on_attach_callback(c.callback_args);
} }
@@ -170,14 +169,14 @@ var dom = {
* @return {jQuery} the detached elements * @return {jQuery} the detached elements
*/ */
detach: function (to_detach, options) { detach: function (to_detach, options) {
_.each(to_detach, function (d) { to_detach.forEach( function (d) {
if (d.widget.on_detach_callback) { if (d.widget.on_detach_callback) {
d.widget.on_detach_callback(d.callback_args); d.widget.on_detach_callback(d.callback_args);
} }
}); });
var $to_detach = options && options.$to_detach; var $to_detach = options && options.$to_detach;
if (!$to_detach) { if (!$to_detach) {
$to_detach = $(_.map(to_detach, function (d) { $to_detach = $(to_detach.map(function (d) {
return d.widget.el; return d.widget.el;
})); }));
} }
@@ -229,7 +228,7 @@ var dom = {
* @param {function|boolean} stopPropagation * @param {function|boolean} stopPropagation
*/ */
makeAsyncHandler: function (fct, preventDefault, 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. // still pending.
var def = $.when(); var def = $.when();
@@ -284,11 +283,13 @@ var dom = {
// a 'real' debounce creation useless. Also, during the debouncing // a 'real' debounce creation useless. Also, during the debouncing
// part, the button is disabled without any visual effect. // part, the button is disabled without any visual effect.
$button.addClass('o_debounce_disabled'); $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.addClass('disabled').prop('disabled', true);
$button.removeClass('o_debounce_disabled'); $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); $button.removeClass('disabled').prop('disabled', false);
}); });
}); });
+2 -2
View File
@@ -114,7 +114,7 @@ function redirect (url, wait) {
}; };
var wait_server = function() { 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); setTimeout(wait_server, 250);
}); });
}; };
@@ -172,7 +172,7 @@ core.action_registry.add("login", login);
function logout() { function logout() {
redirect('/web/session/logout'); redirect('/web/session/logout');
return $.Deferred(); return new Promise();
} }
core.action_registry.add("logout", logout); core.action_registry.add("logout", logout);
+15 -16
View File
@@ -62,37 +62,36 @@ var ParentedMixin = {
* Utility method to only execute asynchronous actions if the current * Utility method to only execute asynchronous actions if the current
* object has not been destroyed. * object has not been destroyed.
* *
* @param {$.Deferred} promise The promise representing the asynchronous * @param {Promise} promise The promise representing the asynchronous
* action. * 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 * rejected with no arguments if the current
* object is destroyed. If false, the * object is destroyed. If false, the
* returned promise will never be resolved * returned promise will never be resolved
* or rejected. * 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 * everything goes fine but will either be rejected
* with no arguments or never resolved if the * with no arguments or never resolved if the
* current object is destroyed. * current object is destroyed.
*/ */
alive: function (promise, reject) { alive: function (promise, shouldReject) {
var self = this; var self = this;
return $.Deferred(function (def) {
promise.then(function () { return new Promise(function (resolve, reject) {
promise.then(function (result) {
if (!self.isDestroyed()) { if (!self.isDestroyed()) {
def.resolve.apply(def, arguments); resolve(result);
} else if (shouldReject) {
reject();
} }
}, function () { }).guardedCatch(function (reason) {
if (!self.isDestroyed()) { if (!self.isDestroyed()) {
def.reject.apply(def, arguments); reject(reason);
} else if (shouldReject) {
reject();
} }
}).always(function () {
if (reject) {
// noop if def already resolved or rejected
def.reject();
}
// otherwise leave promise in limbo
}); });
}).promise(); });
}, },
/** /**
* Inform the object it should destroy itself, releasing any * Inform the object it should destroy itself, releasing any
+6 -5
View File
@@ -76,7 +76,7 @@ var Model = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
* @returns {Promise} The promise resolves to some kind of handle * @returns {Promise} The promise resolves to some kind of handle
*/ */
load: function () { load: function () {
return $.when(); return Promise.resolve();
}, },
}); });
@@ -119,9 +119,9 @@ var Controller = Widget.extend({
* @returns {Promise} * @returns {Promise}
*/ */
start: function () { start: function () {
return $.when( return Promise.all(
this._super.apply(this, arguments), [this._super.apply(this, arguments),
this._startRenderer() this._startRenderer()]
); );
}, },
@@ -177,7 +177,8 @@ var Factory = Class.extend({
getController: function (parent) { getController: function (parent) {
var self = this; var self = this;
var model = this.getModel(parent); 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 renderer = self.getRenderer(parent, state);
var Controller = self.Controller || self.config.Controller; var Controller = self.Controller || self.config.Controller;
var controllerParams = _.extend({ var controllerParams = _.extend({
+1 -1
View File
@@ -11,7 +11,7 @@ return {
* *
* @param {Object} params @see buildQuery for a description * @param {Object} params @see buildQuery for a description
* @param {Object} options * @param {Object} options
* @returns {Deferred<any>} * @returns {Promise<any>}
*/ */
query: function (params, options) { query: function (params, options) {
var query = this.buildQuery(params); var query = this.buildQuery(params);
+51 -41
View File
@@ -124,7 +124,7 @@ var ServicesMixin = {
return result; 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. * the RPC result.
* *
* @param {string} params either a route or a model * @param {string} params either a route or a model
@@ -133,11 +133,17 @@ var ServicesMixin = {
*/ */
_rpc: function (params, options) { _rpc: function (params, options) {
var query = rpc.buildQuery(params); var query = rpc.buildQuery(params);
var def = this.call('ajax', 'rpc', query.route, query.params, options, this) || $.Deferred(); var prom = this.call('ajax', 'rpc', query.route, query.params, options, this);
var promise = def.promise(); if (!prom) {
var abort = (def.abort ? def.abort : def.reject) || function () {}; prom = new Promise(function () {});
promise.abort = abort.bind(def); prom.abort = function () {};
return promise; }
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) { loadFieldView: function (modelName, context, view_id, view_type, options) {
return this.loadViews(modelName, context, [[view_id, view_type]], options).then(function (result) { 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) { loadViews: function (modelName, context, views, options) {
var def = $.Deferred(); var self = this;
this.trigger_up('load_views', { return new Promise(function (resolve) {
modelName: modelName, self.trigger_up('load_views', {
context: context, modelName: modelName,
views: views, context: context,
options: options, views: views,
on_success: def.resolve.bind(def), options: options,
on_success: resolve,
});
}); });
return def;
}, },
loadFilters: function (modelName, actionId, context) { loadFilters: function (modelName, actionId, context) {
var def = $.Deferred(); var self = this;
this.trigger_up('load_filters', { return new Promise(function (resolve, reject) {
modelName: modelName, self.trigger_up('load_filters', {
actionId: actionId, modelName: modelName,
context: context, actionId: actionId,
on_success: def.resolve.bind(def), context: context,
on_success: resolve,
});
}); });
return def;
}, },
createFilter: function (filter) { createFilter: function (filter) {
var def = $.Deferred(); var self = this;
this.trigger_up('create_filter', { return new Promise(function (resolve, reject) {
filter: filter, self.trigger_up('create_filter', {
on_success: def.resolve.bind(def), filter: filter,
on_success: resolve,
});
}); });
return def;
}, },
deleteFilter: function (filterId) { deleteFilter: function (filterId) {
var def = $.Deferred(); var self = this;
this.trigger_up('delete_filter', { return new Promise(function (resolve, reject) {
filterId: filterId, self.trigger_up('delete_filter', {
on_success: def.resolve.bind(def), filterId: filterId,
on_success: resolve,
});
}); });
return def;
}, },
// Session stuff // Session stuff
getSession: function () { getSession: function () {
@@ -195,22 +205,22 @@ var ServicesMixin = {
* Informs the action manager to do an action. This supposes that the action * Informs the action manager to do an action. This supposes that the action
* manager can be found amongst the ancestors of the current widget. * manager can be found amongst the ancestors of the current widget.
* If that's not the case this method will simply return an unresolved * If that's not the case this method will simply return an unresolved
* deferred. * promise.
* *
* @param {any} action * @param {any} action
* @param {any} options * @param {any} options
* @returns {Deferred} * @returns {Promise}
*/ */
do_action: function (action, options) { do_action: function (action, options) {
var def = $.Deferred(); var self = this;
return new Promise(function (resolve, reject) {
this.trigger_up('do_action', { self.trigger_up('do_action', {
action: action, action: action,
options: options, options: options,
on_success: function (result) { def.resolve(result); }, on_success: resolve,
on_fail: function (result) { def.reject(result); }, on_fail: reject,
});
}); });
return def;
}, },
do_notify: function (title, message, sticky, className) { do_notify: function (title, message, sticky, className) {
return this.call('notification', 'notify', {title: title, message: message, sticky: sticky, className: className}); return this.call('notification', 'notify', {title: title, message: message, sticky: sticky, className: className});
+35 -33
View File
@@ -84,27 +84,27 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
*/ */
session_init: function () { session_init: function () {
var self = this; var self = this;
var def = this.session_reload(); var prom = this.session_reload();
if (this.is_frontend) { if (this.is_frontend) {
return def.then(function () { return prom.then(function () {
return self.load_translations(); return self.load_translations();
}); });
} }
return def.then(function () { return prom.then(function () {
var modules = self.module_list.join(','); var modules = self.module_list.join(',');
var deferred = self.load_qweb(modules); var promise = self.load_qweb(modules);
if (self.session_is_valid()) { if (self.session_is_valid()) {
return deferred.then(function () { return self.load_modules(); }); return promise.then(function () { return self.load_modules(); });
} }
return $.when( return Promise.all([
deferred, promise,
self.rpc('/web/webclient/bootstrap_translations', {mods: self.module_list}) self.rpc('/web/webclient/bootstrap_translations', {mods: self.module_list})
.then(function (trans) { .then(function (trans) {
_t.database.set_bundle(trans); _t.database.set_bundle(trans);
}) })
); ]);
}); });
}, },
session_is_valid: function () { session_is_valid: function () {
@@ -119,7 +119,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
*/ */
session_authenticate: function () { session_authenticate: function () {
var self = this; 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(); return self.load_modules();
}); });
}, },
@@ -131,7 +131,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
var params = {db: db, login: login, password: password}; var params = {db: db, login: login, password: password};
return this.rpc("/web/session/authenticate", params).then(function (result) { return this.rpc("/web/session/authenticate", params).then(function (result) {
if (!result.uid) { if (!result.uid) {
return $.Deferred().reject(); return Promise.reject();
} }
_.extend(self, result); _.extend(self, result);
}); });
@@ -142,7 +142,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
}, },
user_has_group: function (group) { user_has_group: function (group) {
if (!this.uid) { if (!this.uid) {
return $.when(false); return Promise.resolve(false);
} }
var def = this._groups_def[group]; var def = this._groups_def[group];
if (!def) { if (!def) {
@@ -196,22 +196,24 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
var to_load = _.difference(modules, self.module_list).join(','); var to_load = _.difference(modules, self.module_list).join(',');
this.module_list = all_modules; 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 locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
var file_list = [ locale ]; var file_list = [ locale ];
if(to_load.length) { if(to_load.length) {
loaded = $.when( loaded = Promise.all([
loaded, 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.load_qweb(to_load),
self.rpc('/web/webclient/jslist', {mods: to_load}).done(function (files) { self.rpc('/web/webclient/jslist', {mods: to_load})
file_list = file_list.concat(files); .then(function (files) {
}) file_list = file_list.concat(files);
); })
]);
} }
return loaded.then(function () { return loaded.then(function () {
return self.load_js(file_list); return self.load_js(file_list);
}).done(function () { }).then(function () {
self.on_modules_loaded(); self.on_modules_loaded();
self.trigger('module_loaded'); self.trigger('module_loaded');
}); });
@@ -227,24 +229,24 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
}, },
load_js: function (files) { load_js: function (files) {
var self = this; var self = this;
var d = $.Deferred(); return new Promise(function (resolve, reject) {
if (files.length !== 0) { if (files.length !== 0) {
var file = files.shift(); var file = files.shift();
var url = self.url(file, null); var url = self.url(file, null);
ajax.loadJS(url).done(d.resolve); ajax.loadJS(url).then(resolve);
} else { } else {
d.resolve(); resolve();
} }
return d; });
}, },
load_qweb: function (mods) { 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) { return $.get('/web/webclient/qweb?mods=' + mods).then(function (doc) {
if (!doc) { return; } if (!doc) { return; }
qweb.add_template(doc); qweb.add_template(doc);
}); });
}); });
return this.qweb_mutex.def; return lock;
}, },
on_modules_loaded: function () { on_modules_loaded: function () {
var openerp = window.openerp; 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 * (re)loads the content of a session: db name, username, user id, session
* context and status of the support contract * 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 () { session_reload: function () {
var result = _.extend({}, window.odoo.session_info); var result = _.extend({}, window.odoo.session_info);
_.extend(this, result); _.extend(this, result);
return $.when(); return Promise.resolve();
}, },
/** /**
* Executes an RPC call, registering the provided callbacks. * 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 {String} url RPC endpoint
* @param {Object} params call parameters * @param {Object} params call parameters
* @param {Object} options additional options for rpc call * @param {Object} options additional options for rpc call
* @returns {jQuery.Deferred} jquery-provided ajax deferred * @returns {Promise}
*/ */
rpc: function (url, params, options) { rpc: function (url, params, options) {
var self = this; var self = this;
+2 -2
View File
@@ -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 @param {Object} [lang] lang The language. If not specified it will default to the language
of the current user. of the current user.
@param {string} [url='/web/webclient/translations'] @param {string} [url='/web/webclient/translations']
@returns {jQuery.Deferred} @returns {Promise}
*/ */
load_translations: function(session, modules, lang, url) { load_translations: function(session, modules, lang, url) {
var self = this; var self = this;
return session.rpc(url || '/web/webclient/translations', { return session.rpc(url || '/web/webclient/translations', {
"mods": modules || null, "mods": modules || null,
"lang": lang || null "lang": lang || null
}).done(function(trans) { }).then(function (trans) {
self.set_bundle(trans); self.set_bundle(trans);
}); });
} }
+21 -13
View File
@@ -31,7 +31,7 @@ var ServicesMixin = require('web.ServicesMixin');
* }, * },
* willStart: function () { * willStart: function () {
* // async work that need to be done before the widget is ready * // 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() { * start: function() {
* // stuff you want to make after the rendering, `this.$el` holds a correct value * // 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 * Method called between @see init and @see start. Performs asynchronous
* calls required by the rendering and the start method. * 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. * executed.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
willStart: function () { willStart: function () {
var defs = []; var proms = [];
if (this.xmlDependencies) { 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); return ajax.loadXML(xmlPath, core.qweb);
})); }));
} }
if (this.jsLibs || this.cssLibs || this.assetLibs) { 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 * Method called after rendering. Mostly used to bind actions, perform
* asynchronous calls, etc... * asynchronous calls, etc...
* *
* By convention, this method should return an object that can be passed to * 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 * Note that, for historic reasons, many widgets still do work in the start
* method that would be more suited to the willStart method. * method that would be more suited to the willStart method.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
return $.when(); return Promise.resolve();
}, },
/** /**
* Destroys the current widget, also destroys all its children before * Destroys the current widget, also destroys all its children before
* destroying itself. * destroying itself.
*/ */
destroy: function () { destroy: function () {
_.invoke(this.getChildren(), 'destroy'); this.getChildren().forEach(function (child) {
if(this.$el) { child.destroy();
});
if (this.$el) {
this.$el.remove(); this.$el.remove();
} }
mixins.PropertiesMixin.destroy.call(this); 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. * Renders the current widget and appends it to the given jQuery object.
* *
* @param {jQuery} target * @param {jQuery} target
* @returns {Promise}
*/ */
appendTo: function (target) { appendTo: function (target) {
var self = this; var self = this;
@@ -198,6 +201,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
* Attach the current widget to a dom element * Attach the current widget to a dom element
* *
* @param {jQuery} target * @param {jQuery} target
* @returns {Promise}
*/ */
attachTo: function (target) { attachTo: function (target) {
var self = this; var self = this;
@@ -234,6 +238,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
* object. * object.
* *
* @param {jQuery} target * @param {jQuery} target
* @returns {Promise}
*/ */
insertAfter: function (target) { insertAfter: function (target) {
var self = this; var self = this;
@@ -246,6 +251,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
* object. * object.
* *
* @param {jQuery} target * @param {jQuery} target
* @returns {Promise}
*/ */
insertBefore: function (target) { insertBefore: function (target) {
var self = this; 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. * Renders the current widget and prepends it to the given jQuery object.
* *
* @param {jQuery} target * @param {jQuery} target
* @returns {Promise}
*/ */
prependTo: function (target) { prependTo: function (target) {
var self = this; 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. * Renders the current widget and replaces the given jQuery object.
* *
* @param target A jQuery object or a Widget instance. * @param target A jQuery object or a Widget instance.
* @returns {Promise}
*/ */
replace: function (target) { replace: function (target) {
return this._widgetRenderAndInsert(_.bind(function (t) { return this._widgetRenderAndInsert(_.bind(function (t) {
@@ -415,7 +423,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
* @private * @private
* @param {function: jQuery -> any} insertion * @param {function: jQuery -> any} insertion
* @param {jQuery} target * @param {jQuery} target
* @returns {Deferred} * @returns {Promise}
*/ */
_widgetRenderAndInsert: function (insertion, target) { _widgetRenderAndInsert: function (insertion, target) {
var self = this; var self = this;
@@ -184,7 +184,7 @@ var AbstractField = Widget.extend({
* When a field widget is appended to the DOM, its start method is called, * 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. * and will automatically call render. Most widgets should not override this.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
var self = this; var self = this;
@@ -236,7 +236,7 @@ var AbstractField = Widget.extend({
* if the value changed but was not notified. * if the value changed but was not notified.
* *
* @abstract * @abstract
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
commitChanges: function () {}, commitChanges: function () {},
/** /**
@@ -290,12 +290,12 @@ var AbstractField = Widget.extend({
* is optional, and may be used by a widget to share information from the * 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 * moment a field change event is triggered to the moment a reset
* operation is applied. * 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 * is complete
*/ */
reset: function (record, event) { reset: function (record, event) {
this._reset(record, event); this._reset(record, event);
return this._render() || $.when(); return this._render() || Promise.resolve();
}, },
/** /**
* Remove the invalid class on a field * Remove the invalid class on a field
@@ -396,7 +396,7 @@ var AbstractField = Widget.extend({
* synchronous. * synchronous.
* *
* @private * @private
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
_render: function () { _render: function () {
if (this.attrs.decorations) { if (this.attrs.decorations) {
@@ -413,7 +413,7 @@ var AbstractField = Widget.extend({
* concrete widget. * concrete widget.
* *
* @private * @private
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
_renderEdit: function () { _renderEdit: function () {
}, },
@@ -422,7 +422,7 @@ var AbstractField = Widget.extend({
* the concrete widget. * the concrete widget.
* *
* @private * @private
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
_renderReadonly: function () { _renderReadonly: function () {
}, },
@@ -457,13 +457,13 @@ var AbstractField = Widget.extend({
* will not notify and not trigger the onchange, even though it was changed. * 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 * @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 * triggered even if the new value is the same as the old one
* @returns {Deferred} * @returns {Promise}
*/ */
_setValue: function (value, options) { _setValue: function (value, options) {
// we try to avoid doing useless work, if the value given has not // we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values. // changed. Note that we compare the unparsed values.
if (this.lastSetValue === value || (this.value === false && value === '')) { if (this.lastSetValue === value || (this.value === false && value === '')) {
return $.when(); return Promise.resolve();
} }
this.lastSetValue = value; this.lastSetValue = value;
try { try {
@@ -472,25 +472,26 @@ var AbstractField = Widget.extend({
} catch (e) { } catch (e) {
this._isValid = false; this._isValid = false;
this.trigger_up('set_dirty', {dataPointID: this.dataPointID}); 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)) { if (!(options && options.forceChange) && this._isSameValue(value)) {
return $.when(); return Promise.resolve();
} }
var def = $.Deferred(); var self = this;
var changes = {}; return new Promise(function (resolve, reject) {
changes[this.name] = value; var changes = {};
this.trigger_up('field_changed', { changes[self.name] = value;
dataPointID: this.dataPointID, self.trigger_up('field_changed', {
changes: changes, dataPointID: self.dataPointID,
viewType: this.viewType, changes: changes,
doNotSetDirty: options && options.doNotSetDirty, viewType: self.viewType,
notifyChange: !options || options.notifyChange !== false, doNotSetDirty: options && options.doNotSetDirty,
allowWarning: options && options.allowWarning, notifyChange: !options || options.notifyChange !== false,
onSuccess: def.resolve.bind(def), allowWarning: options && options.allowWarning,
onFailure: def.reject.bind(def), onSuccess: resolve,
onFailure: reject,
});
}); });
return def;
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
+11 -12
View File
@@ -213,7 +213,7 @@ var InputField = DebouncedField.extend({
// sure they are recomputed. // sure they are recomputed.
this._applyDecorations(); this._applyDecorations();
} }
return $.when(); return Promise.resolve();
} else { } else {
return this._render(); return this._render();
} }
@@ -486,8 +486,6 @@ var LinkButton = AbstractField.extend({
}); });
var FieldDate = InputField.extend({ var FieldDate = InputField.extend({
className: "o_field_date", className: "o_field_date",
tagName: "span", tagName: "span",
@@ -513,7 +511,7 @@ var FieldDate = InputField.extend({
*/ */
start: function () { start: function () {
var self = this; var self = this;
var def; var prom;
if (this.mode === 'edit') { if (this.mode === 'edit') {
this.datewidget = this._makeDatePicker(); this.datewidget = this._makeDatePicker();
this.datewidget.on('datetime_changed', this, function () { this.datewidget.on('datetime_changed', this, function () {
@@ -522,13 +520,13 @@ var FieldDate = InputField.extend({
this._setValue(value); 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.datewidget.$el.addClass(self.$el.attr('class'));
self._prepareInput(self.datewidget.$input); self._prepareInput(self.datewidget.$input);
self._replaceElement(self.datewidget.$el); 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(); this.$el.empty();
// Prepare and add the input // Prepare and add the input
this._prepareInput(this.$input).appendTo(this.$el); var def = this._prepareInput(this.$input).appendTo(this.$el);
if (this.currency) { if (this.currency) {
// Prepare and add the currency symbol // Prepare and add the currency symbol
@@ -739,6 +737,7 @@ var FieldMonetary = InputField.extend({
this.$el.prepend($currencySymbol); this.$el.prepend($currencySymbol);
} }
} }
return def;
}, },
/** /**
* @override * @override
@@ -1136,7 +1135,7 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
*/ */
reset: function () { reset: function () {
var self = this; 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') { if (self.mode === 'edit') {
self.$input.trigger('change'); self.$input.trigger('change');
} }
@@ -2568,7 +2567,7 @@ var JournalDashboardGraph = AbstractField.extend({
if (this._isInDOM) { if (this._isInDOM) {
return this._renderInDOM(); return this._renderInDOM();
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* Render the widget. This function assumes that it is attached to the DOM. * Render the widget. This function assumes that it is attached to the DOM.
@@ -2743,13 +2742,13 @@ var FieldDomain = AbstractField.extend({
/** /**
* @private * @private
* @override _render from AbstractField * @override _render from AbstractField
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
// If there is no model, only change the non-domain-selector content // If there is no model, only change the non-domain-selector content
if (!this._domainModel) { if (!this._domainModel) {
this._replaceContent(); this._replaceContent();
return $.when(); return Promise.resolve();
} }
// Convert char value to array value // Convert char value to array value
@@ -2901,7 +2900,7 @@ var AceEditor = DebouncedField.extend({
/** /**
* @override start from AbstractField (Widget) * @override start from AbstractField (Widget)
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this._startAce(this.$('.ace-view-editor')[0]); this._startAce(this.$('.ace-view-editor')[0]);
@@ -6,7 +6,7 @@ var Registry = require('web.Registry');
return new Registry(); return new Registry();
}); });
odoo.define('web._field_registry', function(require) { odoo.define('web._field_registry', function (require) {
"use strict"; "use strict";
var AbstractField = require('web.AbstractField'); var AbstractField = require('web.AbstractField');
@@ -171,11 +171,11 @@ var FieldMany2One = AbstractField.extend({
* is saved. * is saved.
* *
* @override * @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 * (quick) created
*/ */
commitChanges: function () { commitChanges: function () {
return $.when(this.createDef); return Promise.resolve(this.createDef);
}, },
/** /**
* @override * @override
@@ -208,7 +208,7 @@ var FieldMany2One = AbstractField.extend({
this.isDirty = false; this.isDirty = false;
} }
if (this.isDirty) { if (this.isDirty) {
return $.when(); return Promise.resolve();
} else { } else {
return this._render(); return this._render();
} }
@@ -257,9 +257,9 @@ var FieldMany2One = AbstractField.extend({
if (!source.validation || source.validation.call(self, req.term)) { if (!source.validation || source.validation.call(self, req.term)) {
source.loading = true; 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 // 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.results = results;
source.loading = false; source.loading = false;
resp(self._concatenateAutocompleteResults()); resp(self._concatenateAutocompleteResults());
@@ -366,49 +366,55 @@ var FieldMany2One = AbstractField.extend({
/** /**
* @private * @private
* @param {string} name * @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. * modal is closed.
*/ */
_quickCreate: function (name) { _quickCreate: function (name) {
var self = this; var self = this;
var def = $.Deferred(); var createDone;
this.createDef = this.createDef || $.Deferred();
// called when the record has been quick created, or when the dialog has var def = new Promise(function (resolve, reject) {
// been closed (in the case of a 'slow' create), meaning that the job is self.createDef = new Promise(function (innerResolve) {
// done // called when the record has been quick created, or when the dialog has
var createDone = function () { // been closed (in the case of a 'slow' create), meaning that the job is
def.resolve(); // done
self.createDef.resolve(); createDone = function () {
self.createDef = undefined; innerResolve();
}; resolve();
// called if the quick create is disabled on this many2one, or if the self.createDef = undefined;
// 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();
}); });
this.dp.add(nameCreateDef)
.then(function (result) { // called if the quick create is disabled on this many2one, or if the
if (self.mode === "edit") { // quick creation failed (probably because there are mandatory fields on
self.reinitialize({id: result[0], display_name: result[1]}); // the model)
} var slowCreate = function () {
createDone(); var dialog = self._searchCreatePopup("form", false, self._createContext(name));
}) dialog.on('closed', self, createDone);
.fail(def.reject.bind(def)); };
} else { if (self.nodeOptions.quick_create) {
slowCreate(); 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; return def;
}, },
/** /**
@@ -456,35 +462,32 @@ var FieldMany2One = AbstractField.extend({
* *
* @private * @private
* @param {string} search_val * @param {string} search_val
* @returns {Deferred} * @returns {Promise}
*/ */
_search: function (search_val) { _search: function (search_val) {
var self = this; var self = this;
var def = $.Deferred(); var def = new Promise(function (resolve, reject) {
this.orderer.add(def); var context = self.record.getContext(self.recordParams);
var domain = self.record.getDomain(self.recordParams);
var context = this.record.getContext(this.recordParams); // Add the additionalContext
var domain = this.record.getDomain(this.recordParams); _.extend(context, self.additionalContext);
// Add the additionalContext var blacklisted_ids = self._getSearchBlacklist();
_.extend(context, this.additionalContext); if (blacklisted_ids.length > 0) {
domain.push(['id', 'not in', blacklisted_ids]);
}
var blacklisted_ids = this._getSearchBlacklist(); self._rpc({
if (blacklisted_ids.length > 0) { model: self.field.relation,
domain.push(['id', 'not in', blacklisted_ids]); method: "name_search",
} kwargs: {
name: search_val,
this._rpc({ args: domain,
model: this.field.relation, operator: "ilike",
method: "name_search", limit: self.limit + 1,
kwargs: { context: context,
name: search_val, }}).then(function (result) {
args: domain,
operator: "ilike",
limit: this.limit + 1,
context: context,
}})
.then(function (result) {
// possible selections for the m2o // possible selections for the m2o
var values = _.map(result, function (x) { var values = _.map(result, function (x) {
x[1] = self._getDisplayName(x[1]); 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; var dynamicFilters;
if (results) { if (results) {
var ids = _.map(results, function (x) { 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; return def;
}, },
/** /**
@@ -670,8 +674,9 @@ var FieldMany2One = AbstractField.extend({
readonly: !self.can_write, readonly: !self.can_write,
on_saved: function (record, changed) { on_saved: function (record, changed) {
if (changed) { if (changed) {
self._setValue(self.value.data, {forceChange: true}); self._setValue(self.value.data, {forceChange: true}).then(function() {
self.trigger_up('reload', {db_id: self.value.id}); self.trigger_up('reload', {db_id: self.value.id});
});
} }
}, },
}).open(); }).open();
@@ -868,14 +873,14 @@ var FieldX2Many = AbstractField.extend({
* user if he wants to discard it if necessary. * user if he wants to discard it if necessary.
* *
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
commitChanges: function () { commitChanges: function () {
var self = this; var self = this;
var inEditionRecordID = var inEditionRecordID =
this.renderer this.renderer &&
&& this.renderer.viewType === "list" this.renderer.viewType === "list" &&
&& this.renderer.getEditableRecordID(); this.renderer.getEditableRecordID();
if (inEditionRecordID) { if (inEditionRecordID) {
return this.renderer.commitChanges(inEditionRecordID).then(function () { return this.renderer.commitChanges(inEditionRecordID).then(function () {
return self._saveLine(inEditionRecordID); return self._saveLine(inEditionRecordID);
@@ -894,7 +899,7 @@ var FieldX2Many = AbstractField.extend({
* @param {Object} record * @param {Object} record
* @param {OdooEvent} [ev] an event that triggered the reset action * @param {OdooEvent} [ev] an event that triggered the reset action
* @param {Boolean} [fieldChanged] if true, the widget field has changed * @param {Boolean} [fieldChanged] if true, the widget field has changed
* @returns {Deferred} * @returns {Promise}
*/ */
reset: function (record, ev, fieldChanged) { reset: function (record, ev, fieldChanged) {
// If 'fieldChanged' is false, it means that the reset was triggered by // If 'fieldChanged' is false, it means that the reset was triggered by
@@ -904,7 +909,7 @@ var FieldX2Many = AbstractField.extend({
if (!fieldChanged) { if (!fieldChanged) {
var newEval = this._evalColumnInvisibleFields(); var newEval = this._evalColumnInvisibleFields();
if (_.isEqual(this.currentColInvisibleFields, newEval)) { if (_.isEqual(this.currentColInvisibleFields, newEval)) {
return $.when(); return Promise.resolve();
} }
} else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') { } else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name]; var command = ev.data.changes[this.name];
@@ -994,17 +999,20 @@ var FieldX2Many = AbstractField.extend({
* *
* @override * @override
* @private * @private
* @returns {Deferred|undefined} * @returns {Promise|undefined}
*/ */
_render: function () { _render: function () {
var self = this;
if (!this.view) { if (!this.view) {
return this._super(); return this._super();
} }
if (this.renderer) { if (this.renderer) {
this.currentColInvisibleFields = this._evalColumnInvisibleFields(); this.currentColInvisibleFields = this._evalColumnInvisibleFields();
this.renderer.updateState(this.value, {'columnInvisibleFields': this.currentColInvisibleFields}); return this.renderer.updateState(this.value, {
this.pager.updateState({ size: this.value.count }); columnInvisibleFields: this.currentColInvisibleFields,
return $.when(); }).then(function () {
self.pager.updateState({ size: self.value.count });
});
} }
var arch = this.view.arch; var arch = this.view.arch;
var viewType; var viewType;
@@ -1050,11 +1058,11 @@ var FieldX2Many = AbstractField.extend({
* Prepends the control panel's $el to this widget's $el. * Prepends the control panel's $el to this widget's $el.
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_renderControlPanel: function () { _renderControlPanel: function () {
if (!this.view) { if (!this.view) {
return $.when(); return Promise.resolve();
} }
var self = this; var self = this;
var defs = []; var defs = [];
@@ -1072,7 +1080,7 @@ var FieldX2Many = AbstractField.extend({
validate: function () { validate: function () {
var isList = self.view.arch.tag === 'tree'; var isList = self.view.arch.tag === 'tree';
// TODO: we should have some common method in the basic renderer... // 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) { this.pager.on('pager_changed', this, function (new_state) {
@@ -1089,7 +1097,7 @@ var FieldX2Many = AbstractField.extend({
this._renderButtons(); this._renderButtons();
defs.push(this.pager.appendTo($('<div>'))); // start the pager defs.push(this.pager.appendTo($('<div>'))); // start the pager
defs.push(cpDef); defs.push(cpDef);
return $.when.apply($, defs).then(function () { return Promise.all(defs).then(function () {
self._controlPanel.updateContents({ self._controlPanel.updateContents({
cp_content: { cp_content: {
$buttons: self.$buttons, $buttons: self.$buttons,
@@ -1123,24 +1131,24 @@ var FieldX2Many = AbstractField.extend({
* *
* @private * @private
* @param {string} recordID * @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 * rejected if the line could not be saved and the user
* did not agree to discard. * did not agree to discard.
*/ */
_saveLine: function (recordID) { _saveLine: function (recordID) {
var self = this; var self = this;
var def = $.Deferred(); return new Promise(function (resolve, reject) {
var fieldNames = this.renderer.canBeSaved(recordID); var fieldNames = self.renderer.canBeSaved(recordID);
if (fieldNames.length) { if (fieldNames.length) {
this.trigger_up('discard_changes', { self.trigger_up('discard_changes', {
recordID: recordID, recordID: recordID,
onSuccess: def.resolve.bind(def), onSuccess: resolve,
onFailure: def.reject.bind(def), onFailure: reject,
}); });
} else { } else {
this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def)); self.renderer.setRowMode(recordID, 'readonly').then(resolve);
} }
return def.then(function () { }).then(function () {
self.pager.updateState({ size: self.value.count }); self.pager.updateState({ size: self.value.count });
var newEval = self._evalColumnInvisibleFields(); var newEval = self._evalColumnInvisibleFields();
if (!_.isEqual(self.currentColInvisibleFields, newEval)) { if (!_.isEqual(self.currentColInvisibleFields, newEval)) {
@@ -1233,7 +1241,7 @@ var FieldX2Many = AbstractField.extend({
this.trigger_up('edited_list', { id: this.value.id }); this.trigger_up('edited_list', { id: this.value.id });
var editedRecord = this.value.data[ev.data.index]; var editedRecord = this.value.data[ev.data.index];
this.renderer.setRowMode(editedRecord.id, 'edit') this.renderer.setRowMode(editedRecord.id, 'edit')
.done(ev.data.onSuccess); .then(ev.data.onSuccess);
}, },
/** /**
* Updates the given record with the changes. * Updates the given record with the changes.
@@ -1260,11 +1268,11 @@ var FieldX2Many = AbstractField.extend({
operation: 'UPDATE', operation: 'UPDATE',
id: ev.data.dataPointID, id: ev.data.dataPointID,
data: changes, data: changes,
}).done(function () { }).then(function () {
if (ev.data.onSuccess) { if (ev.data.onSuccess) {
ev.data.onSuccess(); ev.data.onSuccess();
} }
}).fail(function () { }).guardedCatch(function () {
if (ev.data.onFailure) { if (ev.data.onFailure) {
ev.data.onFailure(); ev.data.onFailure();
} }
@@ -1332,8 +1340,8 @@ var FieldX2Many = AbstractField.extend({
self.trigger_up('mutexify', { self.trigger_up('mutexify', {
action: function () { action: function () {
return self._saveLine(ev.data.recordID) return self._saveLine(ev.data.recordID)
.done(ev.data.onSuccess) .then(ev.data.onSuccess)
.fail(ev.data.onFailure); .guardedCatch(ev.data.onFailure);
}, },
}); });
}); });
@@ -1361,18 +1369,20 @@ var FieldX2Many = AbstractField.extend({
notifyChange: false, 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 // trigger only once the onchange for parent record
self._setValue({ self._setValue({
operation: 'UPDATE', operation: 'UPDATE',
id: rowID, id: rowID,
data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]), data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]),
}).always(function () { }).then(always).guardedCatch(always);
self.trigger_up('toggle_column_order', {
id: self.value.id,
name: event.data.handleField,
});
});
}); });
}, },
/** /**
@@ -1418,7 +1428,7 @@ var FieldOne2Many = FieldX2Many.extend({
* @override * @override
* @param {Object} record * @param {Object} record
* @param {OdooEvent} [ev] an event that triggered the reset action * @param {OdooEvent} [ev] an event that triggered the reset action
* @returns {Deferred} * @returns {Promise}
*/ */
reset: function (record, ev) { reset: function (record, ev) {
var self = this; var self = this;
@@ -1527,13 +1537,16 @@ var FieldOne2Many = FieldX2Many.extend({
context: data.context, context: data.context,
}, { }, {
allowWarning: data.allowWarning allowWarning: data.allowWarning
}).always(function () { }).then(function () {
self.creatingRecord = false; self.creatingRecord = false;
}).done(function (){ }).then(function (){
if (data.onSuccess){ if (data.onSuccess){
data.onSuccess(); data.onSuccess();
} }
}); }).guardedCatch(function() {
self.creatingRecord = false;
})
;
} }
} else { } else {
this._openFormDialog({ this._openFormDialog({
@@ -1646,8 +1659,10 @@ var FieldMany2Many = FieldX2Many.extend({
domain: this.record.getDomain(this.recordParams), domain: this.record.getDomain(this.recordParams),
fields_view: this.attrs.views && this.attrs.views.form, fields_view: this.attrs.views && this.attrs.views.form,
on_saved: function () { on_saved: function () {
self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true}); self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true})
self.trigger_up('reload', {db_id: ev.data.id}); .then(function () {
self.trigger_up('reload', {db_id: ev.data.id});
});
}, },
on_remove: function () { on_remove: function () {
self._setValue({operation: 'FORGET', ids: [ev.data.id]}); self._setValue({operation: 'FORGET', ids: [ev.data.id]});
@@ -1918,10 +1933,12 @@ var FieldMany2ManyTags = AbstractField.extend({
* @override * @override
*/ */
reset: function (record, event) { reset: function (record, event) {
this._super.apply(this, arguments); var self = this;
if (event && event.target === this) { return this._super.apply(this, arguments).then(function(){
this.activate(); if (event && event.target === self) {
} self.activate();
}
});
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -34,7 +34,7 @@ var AbstractFieldUpgrade = {
* Redirects the user to the odoo-enterprise/uprade page * Redirects the user to the odoo-enterprise/uprade page
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmUpgrade: function () { _confirmUpgrade: function () {
return this._rpc({ return this._rpc({
@@ -49,11 +49,11 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
*/ */
willStart: function () { willStart: function () {
// TODO would be even greater to wait for localeDef only when necessary // TODO would be even greater to wait for localeDef only when necessary
return $.when( return Promise.all([
this._super.apply(this, arguments), this._super.apply(this, arguments),
session.is_bound, session.is_bound,
localeDef localeDef
); ]);
}, },
/** /**
* @override * @override
@@ -82,7 +82,7 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
$('input, textarea').placeholder(); $('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); self.publicWidgets.push(widget);
return widget.attachTo($(el)); 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 * Destroys all registered widget instances. Website would need this before
@@ -259,8 +259,8 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
*/ */
_onWidgetsStartRequest: function (ev) { _onWidgetsStartRequest: function (ev) {
this._startWidgets(ev.data.$target, ev.data.options) this._startWidgets(ev.data.$target, ev.data.options)
.done(ev.data.onSuccess) .then(ev.data.onSuccess)
.fail(ev.data.onFailure); .guardedCatch(ev.data.onFailure);
}, },
/** /**
* Called when the root is notified that the public widgets have to be * Called when the root is notified that the public widgets have to be
@@ -43,7 +43,7 @@ var RootWidget = Widget.extend({
defs.push(this._attachComponents()); defs.push(this._attachComponents());
this._listenToUpdates = true; this._listenToUpdates = true;
return $.when.apply($, defs); return Promise.all(defs);
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -71,7 +71,7 @@ var RootWidget = Widget.extend({
self._widgets.push(w); self._widgets.push(w);
return w.attachTo(element); return w.attachTo(element);
}); });
return $.when.apply($, defs); return Promise.all(defs);
}, },
/** /**
* Instantiates the child widgets that need to be according to the linked * 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) { var defs = _.map(childInfos, function (childInfo) {
return self._attachComponent(childInfo, $from); return self._attachComponent(childInfo, $from);
}); });
return $.when.apply($, defs); return Promise.all(defs);
}, },
/** /**
* Returns the `RootWidgetRegistry` instance that is linked to this * Returns the `RootWidgetRegistry` instance that is linked to this
@@ -37,7 +37,7 @@ var ReportAction = AbstractAction.extend({
start: function () { start: function () {
var self = this; var self = this;
this.iframe = this.$('iframe')[0]; 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 web_base_url = session['web.base.url'];
var trusted_host = utils.get_host_from_url(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); var trusted_protocol = utils.get_protocol_from_url(web_base_url);
@@ -7,19 +7,20 @@ var session = require('web.session');
var AjaxService = AbstractService.extend({ var AjaxService = AbstractService.extend({
rpc: function (route, args, options, target) { rpc: function (route, args, options, target) {
var def = $.Deferred(); var rpcPromise;
var promise = def.promise(); var promise = new Promise(function (resolve, reject) {
var xhrDef = session.rpc(route, args, options); rpcPromise = session.rpc(route, args, options);
promise.abort = xhrDef.abort.bind(xhrDef); rpcPromise.then(function (result) {
xhrDef.then(function () { if (!target.isDestroyed()) {
if (!target.isDestroyed()) { resolve(result);
def.resolve.apply(def, arguments); }
} }).guardedCatch(function (reason) {
}).fail(function () { if (!target.isDestroyed()) {
if (!target.isDestroyed()) { reject(reason);
def.reject.apply(def, arguments); }
} });
}); });
promise.abort = rpcPromise.abort.bind(rpcPromise);
return promise; return promise;
}, },
}); });
+6 -6
View File
@@ -13,15 +13,15 @@ var translation = require('web.translation');
* *
* @type Boolean * @type Boolean
*/ */
var bus = new Bus (); var bus = new Bus();
_.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) { _.each('click,dblclick,keydown,keypress,keyup'.split(','), function (evtype) {
$('html').on(evtype, function(ev) { $('html').on(evtype, function (ev) {
bus.trigger(evtype, ev); bus.trigger(evtype, ev);
}); });
}); });
_.each('resize,scroll'.split(','), function(evtype) { _.each('resize,scroll'.split(','), function (evtype) {
$(window).on(evtype, function(ev) { $(window).on(evtype, function (ev) {
bus.trigger(evtype, ev); bus.trigger(evtype, ev);
}); });
}); });
@@ -37,7 +37,7 @@ return {
_lt: translation._lt, _lt: translation._lt,
// registries // registries
action_registry : new Registry(), action_registry: new Registry(),
crash_registry: new Registry(), crash_registry: new Registry(),
serviceRegistry: new Registry(), serviceRegistry: new Registry(),
/** /**
@@ -65,7 +65,7 @@ var CrashManager = core.Class.extend({
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () { ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
core.bus.trigger('connection_restored'); core.bus.trigger('connection_restored');
self.isConnected = true; self.isConnected = true;
}).fail(function () { }).guardedCatch(function () {
// exponential backoff, with some jitter // exponential backoff, with some jitter
delay = (delay * 1.5) + 500*Math.random(); delay = (delay * 1.5) + 500*Math.random();
setTimeout(checkConnection, delay); setTimeout(checkConnection, delay);
@@ -33,7 +33,7 @@ return core.Class.extend({
* *
* @param {int|string} [action_id] the action id or xmlid * @param {int|string} [action_id] the action id or xmlid
* @param {Object} [additional_context] used to load the action * @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) { load_action: function (action_id, additional_context) {
var self = this; var self = this;
@@ -69,7 +69,7 @@ return core.Class.extend({
* - options.load_filters: whether or not to load the filters, * - options.load_filters: whether or not to load the filters,
* - options.action_id: the action_id (required to load filters), * - options.action_id: the action_id (required to load filters),
* - options.toolbar: whether or not a toolbar will be displayed, * - 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) { load_views: function (params, options) {
var self = this; var self = this;
@@ -108,12 +108,12 @@ return core.Class.extend({
var fvg = result.fields_views[view_descr[1]]; var fvg = result.fields_views[view_descr[1]];
fvg.viewFields = fvg.fields; fvg.viewFields = fvg.fields;
fvg.fields = result.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 // Insert filters, if any, into the filters cache
if (result.filters) { if (result.filters) {
self._cache.filters[filters_key] = $.when(result.filters); self._cache.filters[filters_key] = Promise.resolve(result.filters);
} }
return result.fields_views; return result.fields_views;
@@ -130,7 +130,7 @@ return core.Class.extend({
* @param {string} params.modelName * @param {string} params.modelName
* @param {Object} params.context * @param {Object} params.context
* @param {integer} params.actionId * @param {integer} params.actionId
* @return {Deferred} resolved with the requested filters * @return {Promise} resolved with the requested filters
*/ */
load_filters: function (params) { load_filters: function (params) {
var key = this._gen_key(params.modelName, params.actionId); var key = this._gen_key(params.modelName, params.actionId);
@@ -143,7 +143,7 @@ return core.Class.extend({
}, },
model: 'ir.filters', model: 'ir.filters',
method: 'get_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]; return this._cache.filters[key];
}, },
@@ -152,7 +152,7 @@ return core.Class.extend({
* Calls 'create_or_replace' on 'ir_filters'. * Calls 'create_or_replace' on 'ir_filters'.
* *
* @param {Object} [filter] the filter description * @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) { create_filter: function (filter) {
var self = this; var self = this;
@@ -175,7 +175,7 @@ return core.Class.extend({
* Calls 'unlink' on 'ir_filters'. * Calls 'unlink' on 'ir_filters'.
* *
* @param {integer} filterId Id of the filter to remove * @param {integer} filterId Id of the filter to remove
* @return {Deferred} * @return {Promise}
*/ */
delete_filter: function (filterId) { delete_filter: function (filterId) {
var self = this; var self = this;
@@ -15,7 +15,7 @@ var ReportService = AbstractService.extend({
* Checks the state of the installation of wkhtmltopdf on the server. * Checks the state of the installation of wkhtmltopdf on the server.
* Implements an internal cache to do the request only once. * 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'). * (possible values are 'ok', 'broken', 'install', 'upgrade', 'workers').
*/ */
checkWkhtmltopdf: function () { checkWkhtmltopdf: function () {
@@ -62,7 +62,7 @@ var DebugManager = Widget.extend({
this._has_features = false; this._has_features = false;
// whether the current user is an administrator // whether the current user is an administrator
this._is_admin = session.is_system; this._is_admin = session.is_system;
return $.when( return Promise.resolve(
this._rpc({ this._rpc({
model: 'res.users', model: 'res.users',
method: 'check_access_rights', method: 'check_access_rights',
@@ -160,7 +160,7 @@ var DebugManager = Widget.extend({
.append(QWeb.render('WebClient.DebugManager.Global', { .append(QWeb.render('WebClient.DebugManager.Global', {
manager: this, manager: this,
})); }));
return $.when(); return Promise.resolve();
}, },
select_view: function () { select_view: function () {
var self = this; var self = this;
@@ -281,7 +281,7 @@ DebugManager.include({
model: 'ir.model', model: 'ir.model',
method: 'search', method: 'search',
args: [[['model', '=', model]]] args: [[['model', '=', model]]]
}).done(function (ids) { }).then(function (ids) {
self.do_action({ self.do_action({
res_model: 'ir.model.fields', res_model: 'ir.model.fields',
name: _t('View Fields'), name: _t('View Fields'),
@@ -324,7 +324,7 @@ DebugManager.include({
DebugManager.include({ DebugManager.include({
start: function () { start: function () {
this._can_edit_views = false; this._can_edit_views = false;
return $.when( return Promise.all([
this._super(), this._super(),
this._rpc({ this._rpc({
model: 'ir.ui.view', model: 'ir.ui.view',
@@ -334,6 +334,7 @@ DebugManager.include({
.then(function (ar) { .then(function (ar) {
this._can_edit_views = ar; this._can_edit_views = ar;
}.bind(this)) }.bind(this))
]
); );
}, },
update: function (tag, descriptor, widget) { update: function (tag, descriptor, widget) {
@@ -382,7 +383,7 @@ DebugManager.include({
model: this._action.res_model, model: this._action.res_model,
method: 'get_metadata', method: 'get_metadata',
args: [selectedIDs], args: [selectedIDs],
}).done(function(result) { }).then(function(result) {
var metadata = result[0]; var metadata = result[0];
metadata.creator = field_utils.format.many2one(metadata.create_uid); metadata.creator = field_utils.format.many2one(metadata.create_uid);
metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid); metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid);
@@ -515,7 +516,7 @@ DebugManager.include({
true, true,
condition || false, condition || false,
], ],
}).done(function () { d.close(); }); }).then(function () { d.close(); });
}} }}
] ]
}); });
+56 -53
View File
@@ -65,16 +65,16 @@
} }
console.log('Found ', $listOfAppMenuItems.length, 'apps to test'); console.log('Found ', $listOfAppMenuItems.length, 'apps to test');
var testDef = $.when(); var testPromise = Promise.resolve();
testDef = chainDeferred($listOfAppMenuItems, testDef, testApp); testPromise = chainDeferred($listOfAppMenuItems, testPromise, testApp);
return testDef.then(function() { return testPromise.then(function () {
console.log("Successfully tested ", testedApps.length, " apps"); console.log("Successfully tested ", testedApps.length, " apps");
console.log("Successfully tested ", testedMenus.length - testedApps.length, " menus"); console.log("Successfully tested ", testedMenus.length - testedApps.length, " menus");
console.log("test successful"); console.log("test successful");
}).always(function() { console.log("Test took ", (performance.now() - startTime) / 1000, " seconds");
console.log("Test took ", (performance.now() - startTime)/1000, " seconds"); }).catch(function () {
}).fail(function () { console.log("Test took ", (performance.now() - startTime) / 1000, " seconds");
console.error("test failed"); console.error("Error !");
}); });
} }
@@ -86,18 +86,18 @@
* 3 - clicking on each menu * 3 - clicking on each menu
* 3.1 - clicking on each view * 3.1 - clicking on each view
* @param {DomElement} element: the App menu item * @param {DomElement} element: the App menu item
* @returns {Deferred} * @returns {Promise}
*/ */
function testApp(element){ function testApp(element){
console.log("Testing app menu:", element.dataset.menuXmlid); 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); testedApps.push(element.dataset.menuXmlid);
return testMenuItem(element).then(function () { return testMenuItem(element).then(function () {
var $subMenuItems; var $subMenuItems;
$subMenuItems = $('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4'); $subMenuItems = $('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4');
var testMenuDef = $.when(); var testMenuPromise = Promise.resolve();
testMenuDef = chainDeferred($subMenuItems, testMenuDef, testMenuItem); testMenuPromise = chainDeferred($subMenuItems, testMenuPromise, testMenuItem);
return testMenuDef; return testMenuPromise;
}).then(function(){ }).then(function(){
// no effect in community // no effect in community
var $homeMenu = $("nav.o_main_navbar > a.o_menu_toggle.fa-th"); var $homeMenu = $("nav.o_main_navbar > a.o_menu_toggle.fa-th");
@@ -112,10 +112,10 @@
* 2 - Orchestrate the view switch * 2 - Orchestrate the view switch
* *
* @param {DomElement} element: the menu item * @param {DomElement} element: the menu item
* @returns {Deferred} * @returns {Promise}
*/ */
function testMenuItem(element){ 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); console.log("Testing menu", element.innerText.trim(), " ", element.dataset.menuXmlid);
testedMenus.push(element.dataset.menuXmlid); testedMenus.push(element.dataset.menuXmlid);
var startActionCount = clientActionCount; var startActionCount = clientActionCount;
@@ -128,7 +128,7 @@
$modal.modal('hide'); $modal.modal('hide');
isModal = true; isModal = true;
return true; return true;
}; }
return startActionCount != clientActionCount; return startActionCount != clientActionCount;
}).then(function() { }).then(function() {
if (!isModal) { if (!isModal) {
@@ -138,7 +138,7 @@
if (!isModal) { if (!isModal) {
return testViews(); return testViews();
} }
}).fail(function() { }).catch(function () {
console.error("Error while testing", element); console.error("Error while testing", element);
}); });
}; };
@@ -151,21 +151,21 @@
*/ */
function testViews() { function testViews() {
var $switches = $("nav.o_cp_switch_buttons > button:not(.active):visible"); var $switches = $("nav.o_cp_switch_buttons > button:not(.active):visible");
var testSwitchDef = $.when(); var testSwitchPromise = Promise.resolve();
// chainDeferred($switches, testSwitchDef, testViewSwitch # FIXME // chainDeferred($switches, testSwitchPromise, testViewSwitch # FIXME
_.each($switches, function(switchButton) { _.each($switches, function(switchButton) {
testSwitchDef = testSwitchDef.then(function () { testSwitchPromise = testSwitchPromise.then(function () {
// get the view view-type data attribute // get the view view-type data attribute
return testViewSwitch(switchButton.dataset.viewType); return testViewSwitch(switchButton.dataset.viewType);
}); });
}); });
return testSwitchDef; return testSwitchPromise;
} }
/** /**
* Test a view button * Test a view button
* @param {string} viewType: a string for the view type to test (list, kanban ...) * @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){ function testViewSwitch(viewType){
console.log("Testing view switch: ", viewType); console.log("Testing view switch: ", viewType);
@@ -188,20 +188,20 @@
* Click on each filter in the control pannel * Click on each filter in the control pannel
*/ */
function testFilters() { 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 = $('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'); var $filters = $('.o_filters_menu > .o_menu_item');
console.log("Testing " + $filters.length + " filters"); console.log("Testing " + $filters.length + " filters");
var filter_ids = _.compact(_.map($filters, function(f) { return f.dataset.id})); var filter_ids = _.compact(_.map($filters, function(f) { return f.dataset.id}));
_.each(filter_ids, function(filter_id){ _.each(filter_ids, function(filter_id){
filterDef = filterDef.then(function(){ filterProm = filterProm.then(function(){
var currentViewCount = viewUpdateCount; var currentViewCount = viewUpdateCount;
var $filter = $('.o_menu_item[data-id="' + filter_id + '"] a'); var $filter = $('.o_menu_item[data-id="' + filter_id + '"] a');
// with some customized search views, the filter cannot be found // with some customized search views, the filter cannot be found
if ($filter[0] === undefined) { if ($filter[0] === undefined) {
console.warn('Filter with ID ', filter_id , 'cannot be found'); console.warn('Filter with ID ', filter_id , 'cannot be found');
return $.Deferred().resolve(); return Promise.resolve();
}; }
console.log('Clicking on filter "', $filter.text().trim(), '"'); console.log('Clicking on filter "', $filter.text().trim(), '"');
$filter[0].click(); $filter[0].click();
setTimeout(function() { setTimeout(function() {
@@ -220,52 +220,55 @@
}); });
}); });
}); });
return filterDef; return filterProm;
} }
// utility functions // utility functions
/** /**
* Wait a certain amount of time for a condition to occur * Wait a certain amount of time for a condition to occur
* @param stopCondition: a function that returns a boolean * @param {function} stopCondition a function that returns a boolean
* @returns {Deferred} that is rejected if the timeout is exceeded * @returns {Promise} that is rejected if the timeout is exceeded
*/ */
function waitForCondition(stopCondition) { function waitForCondition(stopCondition) {
var def = $.Deferred(); var prom = new Promise(function (resolve, reject) {
var interval = 250; var interval = 250;
var timeLimit = 15000; var timeLimit = 15000;
function checkCondition() {
if (stopCondition()) { function checkCondition() {
def.resolve(); if (stopCondition()) {
} else { resolve();
timeLimit -= interval;
if (timeLimit > 0) {
// recursive call until the resolve or the timeout
setTimeout(checkCondition, interval);
} else { } else {
console.error("Timeout exceeded", stopCondition); timeLimit -= interval;
def.reject(); if (timeLimit > 0) {
// recursive call until the resolve or the timeout
setTimeout(checkCondition, interval);
} else {
console.error("Timeout exceeded", stopCondition);
reject();
}
} }
} }
} setTimeout(checkCondition, interval);
setTimeout(checkCondition, interval); });
return def; return prom;
}; }
/** /**
* chain deferred actions * 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 {jQueryElement} $elements a list of jquery elements to be passed as arg to the function
* @param f: the function to be deferred * @param {Promise} promise the promise on which other promises will be chained
* @returns : the chained deferred * @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) { _.each($elements, function(el) {
deferred = deferred.then(function () { promise = promise.then(function () {
return f(el); return f(el);
}); });
}); });
return deferred; return promise;
} }
@@ -62,7 +62,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
/** /**
* Simply renders and updates the url. * Simply renders and updates the url.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
var self = this; var self = this;
@@ -70,12 +70,16 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
this.$el.addClass('o_view_controller'); this.$el.addClass('o_view_controller');
return this._super.apply(this, arguments).then(function () { return this._super.apply(this, arguments).then(function () {
var prom;
if (self._controlPanel) { if (self._controlPanel) {
// render the ControlPanel elements (buttons, pager, sidebar...) // render the ControlPanel elements (buttons, pager, sidebar...)
self.controlPanelElements = self._renderControlPanelElements(); prom = self._renderControlPanelElements().then(function (elements) {
self._controlPanel.$el.prependTo(self.$el); self.controlPanelElements = elements;
self._controlPanel.$el.prependTo(self.$el);
});
} }
return Promise.resolve(prom);
}).then(function () {
return self._update(self.initialState); return self._update(self.initialState);
}); });
}, },
@@ -119,7 +123,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*/ */
canBeRemoved: function () { canBeRemoved: function () {
// AAB: get rid of this option when on_hashchange mechanism is improved // 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 * 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 * this method on the active view to make sure it is ok to open the home
* screen (and lose all current state). * 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. * user if he agrees to discard.
* *
* @param {string} [recordID] * @param {string} [recordID]
* if not given, we consider all the changes made by the controller * 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) { discardChanges: function (recordID) {
return $.when(); return Promise.resolve();
}, },
/** /**
* Export the state of the controller containing information that is shared * 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 * Gives the focus to the renderer
*/ */
giveFocus: function() { giveFocus: function () {
this.renderer.giveFocus(); this.renderer.giveFocus();
}, },
/** /**
@@ -174,7 +178,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* Short helper method to reload the view * Short helper method to reload the view
* *
* @param {Object} [params] This object will simply be given to the update * @param {Object} [params] This object will simply be given to the update
* @returns {Deferred} * @returns {Promise}
*/ */
reload: function (params) { reload: function (params) {
params = params || {}; params = params || {};
@@ -186,7 +190,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
params = _.extend({}, params, searchQuery); 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 * 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. * your view does not want a pager, just let this method empty.
* *
* @param {jQuery Node} $node * @param {jQuery Node} $node
* @return {Promise}
*/ */
renderPager: function ($node) { renderPager: function ($node) {
return Promise.resolve();
}, },
/** /**
* Same as renderPager, but for the 'sidebar' zone (the zone with the menu * Same as renderPager, but for the 'sidebar' zone (the zone with the menu
* dropdown in the control panel next to the buttons) * dropdown in the control panel next to the buttons)
* *
* @param {jQuery Node} $node * @param {jQuery Node} $node
* @return {Promise}
*/ */
renderSidebar: function ($node) { renderSidebar: function ($node) {
return Promise.resolve();
}, },
/** /**
* This is the main entry point for the controller. Changes from the search * 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 {Object} [options]
* @param {boolean} [options.reload=true] if true, the model will reload data * @param {boolean} [options.reload=true] if true, the model will reload data
* *
* @returns {Deferred} * @returns {Promise}
*/ */
update: function (params, options) { update: function (params, options) {
var self = this; var self = this;
var shouldReload = (options && 'reload' in options) ? options.reload : true; 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 // 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 // between the start of the update request and the moment the controller
// asks the control panel to update itself ; indeed, it could happen that // 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>'} * return {'html': '<h1>hello, world</h1>'}
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_renderBanner: function () { _renderBanner: function () {
if (this.bannerRoute !== undefined) { if (this.bannerRoute !== undefined) {
var self = this; var self = this;
return this.dp return this.dp
.add(this._rpc({route: this.bannerRoute})) .add(this._rpc({ route: this.bannerRoute }))
.then(function (response) { .then(function (response) {
if (!response.html) { if (!response.html) {
self.$el.removeClass('o_has_banner'); self.$el.removeClass('o_has_banner');
return $.when(); return Promise.resolve();
} }
self.$el.addClass('o_has_banner'); self.$el.addClass('o_has_banner');
var $banner = $(response.html); var $banner = $(response.html);
@@ -313,22 +321,24 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
defs.push(ajax.loadJS(js.src)); defs.push(ajax.loadJS(js.src));
js.remove(); js.remove();
}); });
return $.when.apply($, defs).then(function () { return Promise.all(defs).then(function () {
$banner.prependTo(self.$('> .o_content')); $banner.prependTo(self.$('> .o_content'));
self._$banner = $banner; self._$banner = $banner;
}); });
}); });
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* Renders the control elements (buttons, pager and sidebar) of the current * Renders the control elements (buttons, pager and sidebar) of the current
* view. * view.
* *
* @private * @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 () { _renderControlPanelElements: function () {
var self = this;
var elements = { var elements = {
$buttons: $('<div>'), $buttons: $('<div>'),
$sidebar: $('<div>'), $sidebar: $('<div>'),
@@ -336,15 +346,18 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
}; };
this.renderButtons(elements.$buttons); this.renderButtons(elements.$buttons);
this.renderSidebar(elements.$sidebar); var sidebarProm = this.renderSidebar(elements.$sidebar);
this.renderPager(elements.$pager); var pagerProm = this.renderPager(elements.$pager);
// remove the unnecessary outer div
elements = _.mapObject(elements, function($node) {
return $node && $node.contents();
});
elements.$switch_buttons = this._renderSwitchButtons();
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. * Renders the switch buttons and binds listeners on them.
@@ -354,7 +367,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
*/ */
_renderSwitchButtons: function () { _renderSwitchButtons: function () {
var self = this; var self = this;
var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord}); var views = _.filter(this.actionViews, { multiRecord: this.isMultiRecord });
if (views.length <= 1) { if (views.length <= 1) {
return $(); return $();
@@ -373,7 +386,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button'); var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button');
$switchButtonsFiltered.click(_.debounce(function (event) { $switchButtonsFiltered.click(_.debounce(function (event) {
var viewType = $(event.target).data('view-type'); 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)); }, 200, true));
// set active view's icon as view switcher button's icon in mobile // set active view's icon as view switcher button's icon in mobile
@@ -402,7 +415,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
* *
* @private * @private
* @param {Object} state the state given by the model * @param {Object} state the state given by the model
* @returns {Deferred} * @returns {Promise}
*/ */
_update: function () { _update: function () {
// AAB: update the control panel -> this will be moved elsewhere at some point // 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) { _onOpenRecord: function (ev) {
ev.stopPropagation(); 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', { this.trigger_up('switch_view', {
view_type: 'form', view_type: 'form',
res_id: record.res_id, res_id: record.res_id,
@@ -31,7 +31,7 @@ var AbstractModel = mvc.Model.extend({
* @returns {Promise} * @returns {Promise}
*/ */
reload: function (params) { reload: function (params) {
return $.when(); return Promise.resolve();
}, },
/** /**
* Processes date(time) and selection field values sent by the server. * Processes date(time) and selection field values sent by the server.
@@ -23,14 +23,14 @@ return mvc.Renderer.extend({
this.noContentHelp = params.noContentHelp; 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. * method simply makes sure that we render the view.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this.$el.addClass(this.arch.attrs.class); 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. * Called each time the renderer is attached into the DOM.
@@ -86,11 +86,11 @@ return mvc.Renderer.extend({
* @param {Object} params * @param {Object} params
* @param {boolean} [params.noRender=false] * @param {boolean} [params.noRender=false]
* if true, the method only updates the state without rerendering * if true, the method only updates the state without rerendering
* @returns {Deferred} * @returns {Promise}
*/ */
updateState: function (state, params) { updateState: function (state, params) {
this.state = state; 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 * @abstract
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
return $.when(); return Promise.resolve();
}, },
}); });
@@ -194,11 +194,12 @@ var AbstractView = Factory.extend({
def = this._createControlPanel(parent); def = this._createControlPanel(parent);
} }
var _super = this._super.bind(this); 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 // get the parent of the model if it already exists, as _super will
// set the new controller as parent, which we don't want // set the new controller as parent, which we don't want
var modelParent = self.model && self.model.getParent(); var modelParent = self.model && self.model.getParent();
return _super(parent).done(function (controller) { var prom = _super(parent);
prom.then(function (controller) {
if (controlPanel) { if (controlPanel) {
controlPanel.setParent(controller); controlPanel.setParent(controller);
} }
@@ -207,7 +208,7 @@ var AbstractView = Factory.extend({
self.model.setParent(modelParent); self.model.setParent(modelParent);
} }
}); });
return prom;
}); });
}, },
/** /**
@@ -41,11 +41,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// savingDef is used to ensure that we always wait for pending save // savingDef is used to ensure that we always wait for pending save
// operations to complete before checking if there are changes to // operations to complete before checking if there are changes to
// discard when discardChanges is called // discard when discardChanges is called
this.savingDef = $.when(); this.savingDef = Promise.resolve();
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
// add classname to reflect the (absence of) access rights (used to // add classname to reflect the (absence of) access rights (used to
@@ -66,24 +66,27 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* *
* @override * @override
* @param {string} [recordID] - default to main recordID * @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 * resolved if can be discarded, a boolean value is given to tells
* if there is something to discard or not * if there is something to discard or not
* rejected otherwise * rejected otherwise
*/ */
canBeDiscarded: function (recordID) { canBeDiscarded: function (recordID) {
var self = this;
if (!this.isDirty(recordID)) { 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 message = _t("The record has been modified, your changes will be discarded. Do you want to proceed?");
var def = $.Deferred(); var def;
var dialog = Dialog.confirm(this, message, { def = new Promise(function (resolve, reject) {
title: _t("Warning"), var dialog = Dialog.confirm(self, message, {
confirm_callback: def.resolve.bind(def, true), title: _t("Warning"),
cancel_callback: def.reject.bind(def), confirm_callback: resolve.bind(self, true),
cancel_callback: reject,
});
dialog.on('closed', def, reject);
}); });
dialog.on('closed', def, def.reject);
return def; return def;
}, },
/** /**
@@ -114,7 +117,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @see _.discardChanges * @see _.discardChanges
*/ */
discardChanges: function (recordID, options) { 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)); .then(this._discardChanges.bind(this, recordID || this.handle, options));
}, },
/** /**
@@ -144,6 +147,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @override * @override
*/ */
renderPager: function ($node, options) { renderPager: function ($node, options) {
var self = this;
var data = this.model.get(this.handle, {raw: true}); var data = this.model.get(this.handle, {raw: true});
this.pager = new Pager(this, data.count, data.offset + 1, data.limit, options); 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)); .then(this.pager.enable.bind(this.pager));
}); });
this.pager.appendTo($node); return this.pager.appendTo($node).then(function() {
this._updatePager(); // to force proper visibility self._updatePager(); // to force proper visibility
});
}, },
/** /**
* Saves the record whose ID is given if necessary (@see _saveRecord). * Saves the record whose ID is given if necessary (@see _saveRecord).
* *
* @param {string} [recordID] - default to main recordID * @param {string} [recordID] - default to main recordID
* @param {Object} [options] * @param {Object} [options]
* @returns {Deferred} * @returns {Promise}
* Resolved with the list of field names (whose value has been modified) * Resolved with the list of field names (whose value has been modified)
* Rejected if the record can't be saved * Rejected if the record can't be saved
*/ */
saveRecord: function (recordID, options) { saveRecord: function (recordID, options) {
var self = this;
// Some field widgets can't detect (all) their changes immediately or // Some field widgets can't detect (all) their changes immediately or
// may have to validate them before notifying them, so we ask them to // 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 // 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 // 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 // mutex-protected as commitChanges function of x2m has to be aware of
// all final changes made to a row. // all final changes made to a row.
var self = this; var unlockedMutex = this.mutex.getUnlockedDef()
var savingDef = $.Deferred();
this.savingDef = savingDef;
return this.mutex.getUnlockedDef()
.then(function () { .then(function () {
return self.renderer.commitChanges(recordID || self.handle); return self.renderer.commitChanges(recordID || self.handle);
}) })
.then(function () { .then(function () {
return self.mutex.exec(self._saveRecord.bind(self, recordID, options)); return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
}) });
.then(savingDef.resolve.bind(savingDef)) this.savingDef = new Promise(function (resolve) {
.fail(savingDef.resolve.bind(savingDef)); unlockedMutex.then(resolve).guardedCatch(resolve);
});
return unlockedMutex;
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
update: function (params, options) { update: function (params, options) {
var self = this; var self = this;
@@ -267,31 +272,32 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private * @private
* @param {Object} attrs the attrs of the button clicked * @param {Object} attrs the attrs of the button clicked
* @param {Object} [record] the current state of the view * @param {Object} [record] the current state of the view
* @returns {Deferred} * @returns {Promise}
*/ */
_callButtonAction: function (attrs, record) { _callButtonAction: function (attrs, record) {
var self = this; var self = this;
var def = $.Deferred(); var def = new Promise(function (resolve, reject) {
var reload = function () { var reload = function () {
return self.isDestroyed() ? $.when() : self.reload(); return self.isDestroyed() ? Promise.resolve() : self.reload();
}; };
record = record || this.model.get(this.handle); record = record || self.model.get(self.handle);
this.trigger_up('execute_action', { self.trigger_up('execute_action', {
action_data: _.extend({}, attrs, { action_data: _.extend({}, attrs, {
context: record.getContext({additionalContext: attrs.context || {}}), context: record.getContext({additionalContext: attrs.context || {}}),
}), }),
env: { env: {
context: record.getContext(), context: record.getContext(),
currentID: record.data.id, currentID: record.data.id,
model: record.model, model: record.model,
resIDs: record.res_ids, resIDs: record.res_ids,
}, },
on_success: def.resolve.bind(def), on_success: resolve,
on_fail: function () { on_fail: function () {
self.update({}, {reload: false}).always(def.reject.bind(def)); self.update({}, {reload: false}).then(reject).guardedCatch(reject);
}, },
on_closed: reload, on_closed: reload,
});
}); });
return this.alive(def); 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} id - the id of one of the view's records
* @param {string[]} fields - the changed fields * @param {string[]} fields - the changed fields
* @param {OdooEvent} e - the event that triggered the change * @param {OdooEvent} e - the event that triggered the change
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmChange: function (id, fields, e) { _confirmChange: function (id, fields, e) {
if (e.name === 'discard_changes' && e.target.reset) { 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 * 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 * this function as the URL changes...) @todo get rid of this when
* the webclient/action_manager's hashchange mechanism is improved. * the webclient/action_manager's hashchange mechanism is improved.
* @returns {Deferred} * @returns {Promise}
*/ */
_discardChanges: function (recordID, options) { _discardChanges: function (recordID, options) {
var self = this; var self = this;
@@ -459,7 +465,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {boolean} [options.savePoint=false] * @param {boolean} [options.savePoint=false]
* if true, the record will only be 'locally' saved: its changes * if true, the record will only be 'locally' saved: its changes
* will move from the _changes key to the data key * 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) * Resolved with the list of field names (whose value has been modified)
* Rejected if the record can't be saved * Rejected if the record can't be saved
*/ */
@@ -490,7 +496,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
} }
return saveDef; return saveDef;
} else { } 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 * @private
* @param {string} mode - 'readonly' or 'edit' * @param {string} mode - 'readonly' or 'edit'
* @param {string} [recordID] * @param {string} [recordID]
* @returns {Deferred} * @returns {Promise}
*/ */
_setMode: function (mode, recordID) { _setMode: function (mode, recordID) {
if ((recordID || this.handle) === this.handle) { if ((recordID || this.handle) === this.handle) {
@@ -511,7 +517,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
core.bus.trigger('DOM_updated'); core.bus.trigger('DOM_updated');
}); });
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* Helper method, to get the current environment variables from the model * Helper method, to get the current environment variables from the model
@@ -564,14 +570,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
ev.stopPropagation(); ev.stopPropagation();
var recordID = ev.data.recordID; var recordID = ev.data.recordID;
this._discardChanges(recordID) this._discardChanges(recordID)
.done(function () { .then(function () {
// TODO this will tell the renderer to rerender the widget that // TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click // asked for the discard but will unfortunately lose the click
// made on another row if any // made on another row if any
self._confirmChange(recordID, [ev.data.fieldName], ev) 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, * Forces to save directly the changes if the controller is in readonly,
File diff suppressed because it is too large Load Diff
@@ -61,13 +61,13 @@ var BasicRenderer = AbstractRenderer.extend({
* before notifying them). * before notifying them).
* *
* @param {string} recordID * @param {string} recordID
* @return {Deferred} * @return {Promise}
*/ */
commitChanges: function (recordID) { commitChanges: function (recordID) {
var defs = _.map(this.allFieldWidgets[recordID], function (widget) { var defs = _.map(this.allFieldWidgets[recordID], function (widget) {
return widget.commitChanges(); return widget.commitChanges();
}); });
return $.when.apply($, defs); return Promise.all(defs);
}, },
/** /**
* Updates the internal state of the renderer to the new state. By default, * 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} id
* @param {string[]} fields * @param {string[]} fields
* @param {OdooEvent} ev * @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 * that have been reset
*/ */
confirmChange: function (state, id, fields, ev) { confirmChange: function (state, id, fields, ev) {
@@ -118,7 +118,7 @@ var BasicRenderer = AbstractRenderer.extend({
// state before evaluation // state before evaluation
defs.push(this._updateAllModifiers(record)); defs.push(this._updateAllModifiers(record));
return $.when.apply($, defs).then(function () { return Promise.all(defs).then(function () {
return resetWidgets; return resetWidgets;
}); });
}, },
@@ -295,7 +295,7 @@ var BasicRenderer = AbstractRenderer.extend({
* *
* @private * @private
* @param {AbstractField} widget * @param {AbstractField} widget
* @returns {boolean|Deferred<boolean>} @see AbstractField.isValid * @returns {boolean|Promise<boolean>} @see AbstractField.isValid
*/ */
_canWidgetBeSaved: function (widget) { _canWidgetBeSaved: function (widget) {
var modifiers = this._getEvaluatedModifiers(widget.__node, widget.record); 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 * @param {Object} node the attrs coming from the arch
*/ */
_postProcessField: function (widget, node) { _postProcessField: function (widget, node) {
this._handleAttributes(widget.$el, node);
}, },
/** /**
* Registers or updates the modifiers data associated to the given node. * Registers or updates the modifiers data associated to the given node.
@@ -503,7 +504,7 @@ var BasicRenderer = AbstractRenderer.extend({
* Render the view * Render the view
* *
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
var oldAllFieldWidgets = this.allFieldWidgets; var oldAllFieldWidgets = this.allFieldWidgets;
@@ -520,7 +521,7 @@ var BasicRenderer = AbstractRenderer.extend({
/** /**
* Instantiates the appropriate AbstractField specialization for the given * Instantiates the appropriate AbstractField specialization for the given
* node and prepares its rendering and addition to the DOM. Indeed, the * 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 * be added to the 'defs' attribute. This is supposed to be created and
* deleted by the calling code if necessary. * 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 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 def = widget._widgetRenderAndInsert(function () {});
var async = def.state() === 'pending'; var $el = $('<div>');
var $el = async ? $('<div>') : widget.$el;
if (async) { this.defs.push(def);
this.defs.push(def);
}
// Update the modifiers registration by associating the widget and by // Update the modifiers registration by associating the widget and by
// giving the modifiers options now (as the potential callback is // giving the modifiers options now (as the potential callback is
// associated to new widget) // associated to new widget)
var self = this; var self = this;
def.then(function () { def.then(function () {
if (async) { // when the caller of renderFieldWidget uses something like
$el.replaceWith(widget.$el); // 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, { self._registerModifiers(node, record, widget, {
callback: function (element, modifiers, record) { callback: function (element, modifiers, record) {
element.$el.toggleClass('o_field_empty', !!( element.$el.toggleClass('o_field_empty', !!(
record.data.id record.data.id &&
&& (modifiers.readonly || mode === 'readonly') (modifiers.readonly || mode === 'readonly') &&
&& !element.widget.isSet() !element.widget.isSet()
)); ));
}, },
keepBaseMode: !!options.keepBaseMode, keepBaseMode: !!options.keepBaseMode,
@@ -618,10 +622,10 @@ var BasicRenderer = AbstractRenderer.extend({
* possible. * possible.
* *
* @abstract * @abstract
* @returns {Deferred} * @returns {Promise}
*/ */
_renderView: function () { _renderView: function () {
return $.when(); return Promise.resolve();
}, },
/** /**
* Instantiate custom widgets * Instantiate custom widgets
@@ -637,22 +641,17 @@ var BasicRenderer = AbstractRenderer.extend({
this.widgets.push(widget); 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 def = widget._widgetRenderAndInsert(function () {});
var async = def.state() === 'pending'; this.defs.push(def);
if (async) { var $el = $('<div>');
this.defs.push(def);
}
var $el = async ? $('<div>') : widget.$el;
var self = this; var self = this;
def.then(function () { def.then(function () {
self._handleAttributes(widget.$el, node); self._handleAttributes(widget.$el, node);
self._registerModifiers(node, record, widget); self._registerModifiers(node, record, widget);
widget.$el.addClass('o_widget'); widget.$el.addClass('o_widget');
if (async) { $el.replaceWith(widget.$el);
$el.replaceWith(widget.$el);
}
}); });
return $el; return $el;
@@ -709,7 +708,7 @@ var BasicRenderer = AbstractRenderer.extend({
* *
* @private * @private
* @param {Object} record * @param {Object} record
* @returns {Deferred} resolved once finished * @returns {Promise} resolved once finished
*/ */
_updateAllModifiers: function (record) { _updateAllModifiers: function (record) {
var self = this; var self = this;
@@ -722,7 +721,7 @@ var BasicRenderer = AbstractRenderer.extend({
}); });
delete this.defs; delete this.defs;
return $.when.apply($, defs); return Promise.all(defs);
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -93,7 +93,7 @@ var BasicView = AbstractView.extend({
* *
* @override * @override
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_loadData: function (model) { _loadData: function (model) {
if (this.recordID) { 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); return model.get(record.id);
}); });
} }
@@ -143,7 +143,7 @@ var CalendarController = AbstractController.extend({
* *
* @private * @private
* @param {string} to either 'prev', 'next' or 'today' * @param {string} to either 'prev', 'next' or 'today'
* @returns {Deferred} * @returns {Promise}
*/ */
_move: function (to) { _move: function (to) {
this.model[to](); this.model[to]();
@@ -153,7 +153,7 @@ var CalendarController = AbstractController.extend({
* @private * @private
* @param {Object} record * @param {Object} record
* @param {integer} record.id * @param {integer} record.id
* @returns {Deferred} * @returns {Promise}
*/ */
_updateRecord: function (record) { _updateRecord: function (record) {
return this.model.updateRecord(record).then(this.reload.bind(this, {})); return this.model.updateRecord(record).then(this.reload.bind(this, {}));
@@ -388,18 +388,18 @@ var CalendarController = AbstractController.extend({
self.quick.destroy(); self.quick.destroy();
self.quick = null; self.quick = null;
self.reload(); 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 // This will occurs if there are some more fields required
// Preventdefaulting the error event will prevent the traceback window // Preventdefaulting the error event will prevent the traceback window
errorEvent.preventDefault(); errorEvent.preventDefault();
event.data.options.disableQuickCreate = true; event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick); event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data); self._onOpenCreate(event.data);
})
.always(function () {
self.quickCreating = false; self.quickCreating = false;
}); })
}, },
/** /**
* Called when we want to open or close the sidebar. * Called when we want to open or close the sidebar.
@@ -177,7 +177,7 @@ return AbstractModel.extend({
/** /**
* @override * @override
* @param {any} params * @param {any} params
* @returns {Deferred} * @returns {Promise}
*/ */
load: function (params) { load: function (params) {
var self = this; var self = this;
@@ -199,15 +199,18 @@ return AbstractModel.extend({
// fields to display color, e.g.: user_id.partner_id // fields to display color, e.g.: user_id.partner_id
this.fieldColor = params.fieldColor; this.fieldColor = params.fieldColor;
if (!this.preload_def) { if (!this.preloadPromise) {
this.preload_def = $.Deferred(); this.preloadPromise = new Promise(function (resolve, reject) {
$.when( Promise.all([
this._rpc({model: this.modelName, method: 'check_access_rights', args: ["write", false]}), self._rpc({model: self.modelName, method: 'check_access_rights', args: ["write", false]}),
this._rpc({model: this.modelName, method: 'check_access_rights', args: ["create", false]})) self._rpc({model: self.modelName, method: 'check_access_rights', args: ["create", false]})
.then(function (write, create) { ]).then(function (result) {
self.write_right = write; var write = result[0];
self.create_right = create; var create = result[1];
self.preload_def.resolve(); 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 * Move the current date range to the next period
@@ -247,7 +250,7 @@ return AbstractModel.extend({
* @override * @override
* @param {Object} [params.context] * @param {Object} [params.context]
* @param {Array} [params.domain] * @param {Array} [params.domain]
* @returns {Deferred} * @returns {Promise}
*/ */
reload: function (handle, params) { reload: function (handle, params) {
if (params.domain) { if (params.domain) {
@@ -308,7 +311,7 @@ return AbstractModel.extend({
/** /**
* @param {Object} record * @param {Object} record
* @param {integer} record.id * @param {integer} record.id
* @returns {Deferred} * @returns {Promise}
*/ */
updateRecord: function (record) { updateRecord: function (record) {
// Cannot modify actual name yet // Cannot modify actual name yet
@@ -430,7 +433,7 @@ return AbstractModel.extend({
}, },
/** /**
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_loadCalendar: function () { _loadCalendar: function () {
var self = this; var self = this;
@@ -439,7 +442,7 @@ return AbstractModel.extend({
var defs = _.map(this.data.filters, this._loadFilter.bind(this)); 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({ return self._rpc({
model: self.modelName, model: self.modelName,
method: 'search_read', method: 'search_read',
@@ -450,10 +453,10 @@ return AbstractModel.extend({
.then(function (events) { .then(function (events) {
self._parseServerData(events); self._parseServerData(events);
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self)); self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when( return Promise.all([
self._loadColors(self.data, self.data.data), self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data) self._loadRecordsToFilters(self.data, self.data.data)
); ]);
}); });
}); });
}, },
@@ -461,7 +464,7 @@ return AbstractModel.extend({
* @private * @private
* @param {any} element * @param {any} element
* @param {any} events * @param {any} events
* @returns {Deferred} * @returns {Promise}
*/ */
_loadColors: function (element, events) { _loadColors: function (element, events) {
if (this.fieldColor) { if (this.fieldColor) {
@@ -472,16 +475,16 @@ return AbstractModel.extend({
}); });
this.model_color = this.fields[fieldName].relation || element.model; this.model_color = this.fields[fieldName].relation || element.model;
} }
return $.Deferred().resolve(); return Promise.resolve();
}, },
/** /**
* @private * @private
* @param {any} filter * @param {any} filter
* @returns {Deferred} * @returns {Promise}
*/ */
_loadFilter: function (filter) { _loadFilter: function (filter) {
if (!filter.write_model) { if (!filter.write_model) {
return $.when(); return Promise.resolve();
} }
var field = this.fields[filter.fieldName]; var field = this.fields[filter.fieldName];
@@ -540,7 +543,7 @@ return AbstractModel.extend({
* @private * @private
* @param {any} element * @param {any} element
* @param {any} events * @param {any} events
* @returns {Deferred} * @returns {Promise}
*/ */
_loadRecordsToFilters: function (element, events) { _loadRecordsToFilters: function (element, events) {
var self = this; var self = this;
@@ -608,7 +611,7 @@ return AbstractModel.extend({
to_read[model] = _.object(res); to_read[model] = _.object(res);
})); }));
}); });
return $.when.apply($, defs).then(function () { return Promise.all(defs).then(function () {
_.each(self.data.filters, function (filter) { _.each(self.data.filters, function (filter) {
if (filter.write_model) { if (filter.write_model) {
return; return;
@@ -85,7 +85,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
}); });
defs.push(def); defs.push(def);
} }
return $.when.apply($, defs); return Promise.all(defs);
}, },
/** /**
@@ -193,7 +193,7 @@ return AbstractRenderer.extend({
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this._initSidebar(); this._initSidebar();
@@ -460,7 +460,7 @@ return AbstractRenderer.extend({
* *
* @override method from AbstractRenderer * @override method from AbstractRenderer
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
var $calendar = this.$calendar; var $calendar = this.$calendar;
@@ -504,7 +504,7 @@ return AbstractRenderer.extend({
this.$sidebar_container.toggleClass('o_sidebar_hidden', fullWidth); this.$sidebar_container.toggleClass('o_sidebar_hidden', fullWidth);
this.$sidebar.toggleClass('o_hidden', fullWidth); this.$sidebar.toggleClass('o_hidden', fullWidth);
this._renderFilters(); var filterProm = this._renderFilters();
this.$calendar.appendTo('body'); this.$calendar.appendTo('body');
if (scrollTop) { if (scrollTop) {
this.$calendar.fullCalendar('reinitView'); this.$calendar.fullCalendar('reinitView');
@@ -514,7 +514,7 @@ return AbstractRenderer.extend({
this._renderEvents(); this._renderEvents();
this.$calendar.prependTo(this.$('.o_calendar_view')); 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 * Render all events
@@ -529,25 +529,47 @@ return AbstractRenderer.extend({
* Render all filters * Render all filters
* *
* @private * @private
* @returns {Promise} resolved when all filters have been rendered
*/ */
_renderFilters: function () { _renderFilters: function () {
var self = this;
_.each(this.filters || (this.filters = []), function (filter) { _.each(this.filters || (this.filters = []), function (filter) {
filter.destroy(); filter.destroy();
}); });
if (this.state.fullWidth) { if (this.state.fullWidth) {
return; 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;})) { if (!_.find(options.filters, function (f) {return f.display == null || f.display;})) {
return; 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); var filter = new SidebarFilter(self, options);
filter.appendTo(self.$sidebar); prom = filter.appendTo(this.$sidebar).then(function () {
self.filters.push(filter); return self._renderFiltersOneByOne(filterIndex + 1);
}); });
this.filters.push(filter);
}
return Promise.resolve(prom);
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -63,14 +63,14 @@ var ControlPanelController = mvc.Controller.extend({
}, },
/** /**
* @param {Object} state a ControlPanelModel state * @param {Object} state a ControlPanelModel state
* @returns {Deferred<Object>} the result of `getSearchState` * @returns {Promise<Object>} the result of `getSearchState`
*/ */
importState: function (state) { importState: function (state) {
var defs = []; var defs = [];
this.model.importState(state); this.model.importState(state);
defs.push(this.getSearchQuery()); defs.push(this.getSearchQuery());
defs.push(this.renderer.updateState(this.model.get())); defs.push(this.renderer.updateState(this.model.get()));
return $.when(defs).then(function (defsResults) { return Promise.all(defs).then(function (defsResults) {
return defsResults[0]; return defsResults[0];
}); });
}, },
@@ -127,7 +127,7 @@ var ControlPanelController = mvc.Controller.extend({
}, },
/** /**
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_reportNewQueryAndRender: function () { _reportNewQueryAndRender: function () {
this.trigger_up('search', this.model.getQuery()); this.trigger_up('search', this.model.getQuery());
@@ -325,7 +325,7 @@ var ControlPanelModel = mvc.Model.extend({
* @param {string[]} [params.searchMenuTypes=[]] * @param {string[]} [params.searchMenuTypes=[]]
* @param {Object} [params.timeRanges] * @param {Object} [params.timeRanges]
* @param {boolean} [params.withSearchBar] * @param {boolean} [params.withSearchBar]
* @returns {Deferred} * @returns {Promise}
*/ */
load: function (params) { load: function (params) {
var self = this; var self = this;
@@ -337,11 +337,11 @@ var ControlPanelModel = mvc.Model.extend({
// and the info comming from arch put in params.groups (if any) // and the info comming from arch put in params.groups (if any)
// will be lost. This is not a problem because the state // will be lost. This is not a problem because the state
// won't be use elsewhere. // won't be use elsewhere.
return $.when(); return Promise.resolve();
} }
if (params.initialState) { if (params.initialState) {
this.importState(params.initialState); this.importState(params.initialState);
return $.when(); return Promise.resolve();
} else { } else {
var groups = params.groups || []; var groups = params.groups || [];
groups.forEach(function (group) { groups.forEach(function (group) {
@@ -351,7 +351,7 @@ var ControlPanelModel = mvc.Model.extend({
this._createEmptyGroup('groupBy'); this._createEmptyGroup('groupBy');
} }
this._createGroupOfTimeRanges(); this._createGroupOfTimeRanges();
return $.when.apply($, self._loadSearchDefaults()).then(function () { return Promise.all(self._loadSearchDefaults()).then(function () {
return self._loadFavorites().then(function () { return self._loadFavorites().then(function () {
if (self.query.length === 0) { if (self.query.length === 0) {
self._activateDefaultFilters(); self._activateDefaultFilters();
@@ -910,7 +910,7 @@ var ControlPanelModel = mvc.Model.extend({
* if this.activateDefaultFavorite is true. * if this.activateDefaultFavorite is true.
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_loadFavorites: function () { _loadFavorites: function () {
var self = this; var self = this;
@@ -980,7 +980,7 @@ var ControlPanelModel = mvc.Model.extend({
* is asynchronous. * is asynchronous.
* *
* @private * @private
* @returns {Deferred[]} * @returns {Promise[]}
*/ */
_loadSearchDefaults: function () { _loadSearchDefaults: function () {
var self = this; var self = this;
@@ -1044,7 +1044,7 @@ var ControlPanelModel = mvc.Model.extend({
* *
* @private * @private
* @param {Object} favorite * @param {Object} favorite
* @returns {Deferred} * @returns {Promise}
*/ */
_saveQuery: function (favorite) { _saveQuery: function (favorite) {
var self = this; var self = this;
@@ -86,7 +86,7 @@ var ControlPanelRenderer = Renderer.extend({
var superDef = this._super.apply(this, arguments); var superDef = this._super.apply(this, arguments);
var searchDef = this._renderSearch(); var searchDef = this._renderSearch();
return $.when(superDef, searchDef).then(function () { return Promise.all([superDef, searchDef]).then(function () {
self._setSearchMenusVisibility(); self._setSearchMenusVisibility();
}); });
}, },
@@ -148,7 +148,7 @@ var ControlPanelRenderer = Renderer.extend({
* Update the state of the renderer state. It retriggers a full rerendering. * Update the state of the renderer state. It retriggers a full rerendering.
* *
* @param {Object} state * @param {Object} state
* @returns {Deferred} * @returns {Promise}
*/ */
updateState: function (state) { updateState: function (state) {
this.state = state; this.state = state;
@@ -261,7 +261,7 @@ var ControlPanelRenderer = Renderer.extend({
* Renderer the search bar and the search menus * Renderer the search bar and the search menus
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_renderSearch: function () { _renderSearch: function () {
var defs = []; var defs = [];
@@ -274,11 +274,11 @@ var ControlPanelRenderer = Renderer.extend({
if (this.withSearchBar) { if (this.withSearchBar) {
defs.push(this._renderSearchBar()); defs.push(this._renderSearchBar());
} }
return $.when(this, defs).then(this._focusSearchInput.bind(this)); return Promise.all(defs).then(this._focusSearchInput.bind(this));
}, },
/** /**
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_renderSearchBar: function () { _renderSearchBar: function () {
// TODO: might need a reload instead of a destroy/instantiate // TODO: might need a reload instead of a destroy/instantiate
@@ -315,7 +315,7 @@ var ControlPanelRenderer = Renderer.extend({
* *
* @private * @private
* @param {string} menuType * @param {string} menuType
* @returns {Deferred} * @returns {Promise}
*/ */
_setupMenu: function (menuType) { _setupMenu: function (menuType) {
var Menu; var Menu;
@@ -345,7 +345,7 @@ var ControlPanelRenderer = Renderer.extend({
* Instantiate the search menu determined by this.searchMenuTypes. * Instantiate the search menu determined by this.searchMenuTypes.
* *
* @private * @private
* @returns {Deferred[]} * @returns {Promise[]}
*/ */
_setupMenus: function () { _setupMenus: function () {
this.$subMenus = this._getSubMenusPlace(); this.$subMenus = this._getSubMenusPlace();
@@ -67,14 +67,14 @@ var FilterMenu = DropdownMenu.extend({
* Add a proposition inside the custom filter edition menu. * Add a proposition inside the custom filter edition menu.
* *
* @private * @private
* @returns {$.Deferred} * @returns {Promise}
*/ */
_appendProposition: function () { _appendProposition: function () {
// make modern sear_filters code!!! It works but... // make modern sear_filters code!!! It works but...
var prop = new search_filters.ExtendedSearchProposition(this, this.fields); var prop = new search_filters.ExtendedSearchProposition(this, this.fields);
this.propositions.push(prop); this.propositions.push(prop);
this.$('.o_apply_filter').prop('disabled', false); 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. * Confirm a filter proposition, creates it and add it to the menu.
@@ -46,7 +46,7 @@ var SearchBar = Widget.extend({
defs.push(self._renderFacet(facet)); defs.push(self._renderFacet(facet));
}); });
defs.push(this._setupAutoCompletion()); defs.push(this._setupAutoCompletion());
return $.when.apply($, defs); return Promise.all(defs);
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -116,18 +116,21 @@ var SearchBar = Widget.extend({
* @param {Function} callback * @param {Function} callback
*/ */
_getAutoCompleteSources: function (req, callback) { _getAutoCompleteSources: function (req, callback) {
var defs = _.invoke(this.autoCompleteSources, 'getAutocompletionValues', req.term); var defs = this.autoCompleteSources.map(function (source) {
$.when.apply($, defs).then(function () { return source.getAutocompletionValues(req.term);
callback(_(arguments).chain() });
Promise.all(defs).then(function (result) {
var resultCleaned = _(result).chain()
.compact() .compact()
.flatten(true) .flatten(true)
.value()); .value();
}); callback(resultCleaned);
});
}, },
/** /**
* @private * @private
* @param {Object} facet * @param {Object} facet
* @returns {Deferred} * @returns {Promise}
*/ */
_renderFacet: function (facet) { _renderFacet: function (facet) {
var searchFacet = new SearchFacet(this, facet); var searchFacet = new SearchFacet(this, facet);
@@ -136,7 +139,7 @@ var SearchBar = Widget.extend({
}, },
/** /**
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_setupAutoCompletion: function () { _setupAutoCompletion: function () {
var self = this; var self = this;
@@ -32,7 +32,7 @@ var FilterInterface = Class.extend(mixins.EventDispatcherMixin, {
* label. They will be used by search_bar in @_onAutoCompleteSelected. * label. They will be used by search_bar in @_onAutoCompleteSelected.
* *
* @param {string} value value to getAutocompletionValues * @param {string} value value to getAutocompletionValues
* @returns {Deferred<null|Array>} * @returns {Promise<null|Array>}
*/ */
getAutocompletionValues: function (value) { getAutocompletionValues: function (value) {
var result; 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 * @override
*/ */
getAutocompletionValues: function (value) { getAutocompletionValues: function (value) {
return $.when([{ return Promise.resolve([{
label: this._getAutocompletionLabel(value), label: this._getAutocompletionLabel(value),
facet: this._getFacetValue(value), facet: this._getFacetValue(value),
}]); }]);
@@ -194,7 +194,7 @@ var CharField = Field.extend({
* @override * @override
*/ */
getAutocompletionValues: function (value) { getAutocompletionValues: function (value) {
if (_.isEmpty(value)) { return $.when(null); } if (_.isEmpty(value)) { return Promise.resolve(null); }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
}); });
@@ -210,7 +210,7 @@ var NumberField = Field.extend({
*/ */
getAutocompletionValues: function (value) { getAutocompletionValues: function (value) {
var val = this.parse(value); var val = this.parse(value);
if (isNaN(val)) { return $.when(); } if (isNaN(val)) { return Promise.resolve(); }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
}); });
@@ -261,8 +261,8 @@ var SelectionField = Field.extend({
facet: self._getFacetValue(sel) facet: self._getFacetValue(sel)
}; };
}).value(); }).value();
if (_.isEmpty(results)) { return $.when(null); } if (_.isEmpty(results)) { return Promise.resolve(null); }
return $.when.call(null, [{ return Promise.resolve([{
label: _.escape(this.attrs.string) label: _.escape(this.attrs.string)
}].concat(results)); }].concat(results));
}, },
@@ -310,15 +310,15 @@ var DateField = Field.extend({
t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date'; t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
v = field_utils.parse[t](value, {type: t}, {timezone: true}); v = field_utils.parse[t](value, {type: t}, {timezone: true});
} catch (e) { } catch (e) {
return $.when(null); return Promise.resolve(null);
} }
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'); 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 dateString = field_utils.format[t](m, {type: t});
var label = this._getAutocompletionLabel(dateString); var label = this._getAutocompletionLabel(dateString);
var facet = this._getFacetValue(dateString, m.toDate()); var facet = this._getFacetValue(dateString, m.toDate());
return $.when([{ return Promise.resolve([{
label: label, label: label,
facet: facet, facet: facet,
}]); }]);
@@ -379,9 +379,9 @@ var ManyToOneField = CharField.extend({
* @override * @override
*/ */
getAutocompletionValues: function (value) { getAutocompletionValues: function (value) {
if (_.isEmpty(value)) { return $.when(null); } if (_.isEmpty(value)) { return Promise.resolve(null); }
var label = this._getAutocompletionLabel(value); var label = this._getAutocompletionLabel(value);
return $.when([{ return Promise.resolve([{
label: label, label: label,
facet: this._getFacetValue(value), facet: this._getFacetValue(value),
expand: this._expand.bind(this), expand: this._expand.bind(this),
@@ -40,7 +40,9 @@ var ExtendedSearchProposition = Widget.extend({
this.value = null; this.value = null;
}, },
start: function () { start: function () {
return this._super().done(this.proxy('changed')); var parent = this._super();
parent.then(this.proxy('changed'));
return parent;
}, },
changed: function () { changed: function () {
var nval = this.$(".o_searchview_extended_prop_field").val(); var nval = this.$(".o_searchview_extended_prop_field").val();
@@ -234,10 +236,10 @@ var DateTime = Field.extend({
return str; return str;
}, },
start: function () { start: function () {
return $.when( return Promise.all([
this._super.apply(this, arguments), this._super.apply(this, arguments),
this._create_new_widget("datewidget_0") this._create_new_widget("datewidget_0")
); ]);
}, },
_create_new_widget: function (name) { _create_new_widget: function (name) {
this[name] = new (this._get_widget_class())(this); this[name] = new (this._get_widget_class())(this);
@@ -41,7 +41,7 @@ var FieldManagerMixin = {
* @param {string} dataPointID * @param {string} dataPointID
* @param {Object} changes * @param {Object} changes
* @param {OdooEvent} event * @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 * updated
*/ */
_applyChanges: function (dataPointID, changes, event) { _applyChanges: function (dataPointID, changes, event) {
@@ -66,10 +66,10 @@ var FieldManagerMixin = {
* @param {string} id basicModel Id for the changed record * @param {string} id basicModel Id for the changed record
* @param {string[]} fields the fields (names) that have been changed * @param {string[]} fields the fields (names) that have been changed
* @param {OdooEvent} event the event that triggered the change * @param {OdooEvent} event the event that triggered the change
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmChange: function (id, fields, event) { _confirmChange: function (id, fields, event) {
return $.when(); return Promise.resolve();
}, },
/** /**
* This method will be called whenever a save has been triggered by a change * This method will be called whenever a save has been triggered by a change
@@ -79,10 +79,10 @@ var FieldManagerMixin = {
* @see _onFieldChanged * @see _onFieldChanged
* @abstract * @abstract
* @param {string} id The basicModel ID for the saved record * @param {string} id The basicModel ID for the saved record
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmSave: function (id) { _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 // 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 // but its model doesn't have any data related to the given dataPointID
event.stopPropagation(); event.stopPropagation();
this._applyChanges(event.data.dataPointID, event.data.changes, event) return this._applyChanges(event.data.dataPointID, event.data.changes, event)
.done(event.data.onSuccess || function () {}) .then(event.data.onSuccess || function () {})
.fail(event.data.onFailure || function () {}); .guardedCatch(event.data.onFailure || function () {});
}, },
/** /**
* Some widgets need to trigger a reload of their data. For example, a * Some widgets need to trigger a reload of their data. For example, a
@@ -84,7 +84,7 @@ var FormController = BasicController.extend({
* @todo make record creation a basic controller feature * @todo make record creation a basic controller feature
* @param {string} [parentID] if given, the parentID will be used as parent * @param {string} [parentID] if given, the parentID will be used as parent
* for the new record. * for the new record.
* @returns {Deferred} * @returns {Promise}
*/ */
createRecord: function (parentID) { createRecord: function (parentID) {
var self = this; var self = this;
@@ -165,20 +165,23 @@ var FormController = BasicController.extend({
* @override method from BasicController * @override method from BasicController
* @param {jQueryElement} $node * @param {jQueryElement} $node
* @param {Object} options * @param {Object} options
* @returns {Promise}
*/ */
renderPager: function ($node, options) { renderPager: function ($node, options) {
options = _.extend({}, options, { options = _.extend({}, options, {
validate: this.canBeDiscarded.bind(this), validate: this.canBeDiscarded.bind(this),
}); });
this._super($node, options); return this._super($node, options);
}, },
/** /**
* Instantiate and render the sidebar if a sidebar is requested * Instantiate and render the sidebar if a sidebar is requested
* Sets this.sidebar * Sets this.sidebar
* @param {jQuery} [$node] a jQuery node where the sidebar should be * @param {jQuery} [$node] a jQuery node where the sidebar should be
* inserted * inserted
* @return {Promise}
**/ **/
renderSidebar: function ($node) { renderSidebar: function ($node) {
var self = this;
if (this.hasSidebar) { if (this.hasSidebar) {
var otherItems = []; var otherItems = [];
if (this.is_action_enabled('delete')) { if (this.is_action_enabled('delete')) {
@@ -203,11 +206,12 @@ var FormController = BasicController.extend({
}, },
actions: _.extend(this.toolbarActions, {other: otherItems}), actions: _.extend(this.toolbarActions, {other: otherItems}),
}); });
this.sidebar.appendTo($node); return this.sidebar.appendTo($node).then(function() {
// Show or hide the sidebar according to the view mode
// Show or hide the sidebar according to the view mode self._updateSidebar();
this._updateSidebar(); });
} }
return Promise.resolve();
}, },
/** /**
* Show a warning message if the user modified a translated field. For each * Show a warning message if the user modified a translated field. For each
@@ -296,7 +300,7 @@ var FormController = BasicController.extend({
* @private * @private
* @override method from field manager mixin * @override method from field manager mixin
* @param {string} id - id of the previously changed record * @param {string} id - id of the previously changed record
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmSave: function (id) { _confirmSave: function (id) {
if (id === this.handle) { if (id === this.handle) {
@@ -393,14 +397,19 @@ var FormController = BasicController.extend({
* @override * @override
* @private * @private
* @param {Object} state * @param {Object} state
* @returns {Deferred} * @returns {Promise}
*/ */
_update: function () { _update: function () {
var title = this.getTitle(); var self = this;
this._setTitle(title);
this._updateButtons(); return this._super.apply(this, arguments).then(function() {
this._updateSidebar(); var title = self.getTitle();
return this._super.apply(this, arguments).then(this.autofocus.bind(this)); self._setTitle(title);
self._updateButtons();
self._updateSidebar();
self.autofocus();
});
}, },
/** /**
* @private * @private
@@ -470,13 +479,11 @@ var FormController = BasicController.extend({
} }
var attrs = ev.data.attrs; var attrs = ev.data.attrs;
if (attrs.confirm) { if (attrs.confirm) {
var d = $.Deferred(); def = new Promise(function (resolve, reject) {
Dialog.confirm(this, attrs.confirm, { Dialog.confirm(this, attrs.confirm, {
confirm_callback: saveAndExecuteAction, confirm_callback: saveAndExecuteAction,
}).on("closed", null, function () { }).on("closed", null, resolve);
d.resolve();
}); });
def = d.promise();
} else if (attrs.special === 'cancel') { } else if (attrs.special === 'cancel') {
def = this._callButtonAction(attrs, ev.data.record); def = this._callButtonAction(attrs, ev.data.record);
} else if (!attrs.special || attrs.special === 'save') { } else if (!attrs.special || attrs.special === 'save') {
@@ -484,7 +491,7 @@ var FormController = BasicController.extend({
def = saveAndExecuteAction(); 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 * 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 ev.stopPropagation(); // Prevent x2m lines to be auto-saved
var self = this; var self = this;
this._disableButtons(); this._disableButtons();
this.saveRecord().always(function () { this.saveRecord().then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
self._enableButtons();
});
}, },
/** /**
* Called when user swipes left. Move to next record. * Called when user swipes left. Move to next record.
@@ -251,7 +251,7 @@ var FormRenderer = BasicRenderer.extend({
* @param {string} [params.mode] new mode, either 'edit' or 'readonly' * @param {string} [params.mode] new mode, either 'edit' or 'readonly'
* @param {string[]} [params.fieldNames] if given, the renderer will only * @param {string[]} [params.fieldNames] if given, the renderer will only
* update the fields in this list * update the fields in this list
* @returns {Deferred} * @returns {Promise}
*/ */
updateState: function (state, params) { updateState: function (state, params) {
this.mode = (params && 'mode' in params) ? params.mode : this.mode; this.mode = (params && 'mode' in params) ? params.mode : this.mode;
@@ -342,8 +342,8 @@ var FormRenderer = BasicRenderer.extend({
* @private * @private
*/ */
_postProcessField: function (widget, node) { _postProcessField: function (widget, node) {
this._super.apply(this, arguments);
this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name)); this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
this._handleAttributes(widget.$el, node);
if (JSON.parse(node.attrs.default_focus || "0")) { if (JSON.parse(node.attrs.default_focus || "0")) {
this.defaultFocusField = widget; this.defaultFocusField = widget;
} }
@@ -355,7 +355,14 @@ var FormRenderer = BasicRenderer.extend({
*/ */
_renderButtonBox: function (node) { _renderButtonBox: function (node) {
var self = this; 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) { var buttons = _.map(node.children, function (child) {
if (child.tag === 'button') { if (child.tag === 'button') {
return self._renderStatButton(child); return self._renderStatButton(child);
@@ -363,48 +370,62 @@ var FormRenderer = BasicRenderer.extend({
return self._renderNode(child); 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 defs = this.defs.slice(nextDefIndex);
var visible_buttons = buttons_partition[1]; Promise.all(defs).then(function () {
buttons = $tempWrapper.children();
// Get the unfolded buttons according to window size var buttons_partition = _.partition(buttons, function (button) {
var nb_buttons = [2, 2, 4, 6][config.device.size_class] || 7; return $(button).is('.o_invisible_modifier');
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 = [];
}
// 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 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 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 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);
// 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"),
}));
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._handleAttributes($result, node);
this._registerModifiers(node, this.state, $result); this._registerModifiers(node, this.state, $result);
@@ -477,6 +498,7 @@ var FormRenderer = BasicRenderer.extend({
_renderInnerGroup: function (node) { _renderInnerGroup: function (node) {
var self = this; var self = this;
var $result = $('<table/>', {class: 'o_group o_inner_group'}); var $result = $('<table/>', {class: 'o_group o_inner_group'});
var $tbody = $('<tbody />').appendTo($result);
this._handleAttributes($result, node); this._handleAttributes($result, node);
this._registerModifiers(node, this.state, $result); this._registerModifiers(node, this.state, $result);
@@ -490,7 +512,7 @@ var FormRenderer = BasicRenderer.extend({
var rows = []; var rows = [];
var $currentRow = $('<tr/>'); var $currentRow = $('<tr/>');
var currentColspan = 0; var currentColspan = 0;
_.each(node.children, function (child) { node.children.forEach(function (child) {
if (child.tag === 'newline') { if (child.tag === 'newline') {
rows.push($currentRow); rows.push($currentRow);
$currentRow = $('<tr/>'); $currentRow = $('<tr/>');
@@ -537,7 +559,7 @@ var FormRenderer = BasicRenderer.extend({
var $el = $(el); var $el = $(el);
$el.css('width', ((parseInt($el.attr('colspan'), 10) || 1) * nonLabelColSize) + '%'); $el.css('width', ((parseInt($el.attr('colspan'), 10) || 1) * nonLabelColSize) + '%');
}); });
$result.append($tr); $tbody.append($tr);
}); });
return $result; return $result;
@@ -575,7 +597,7 @@ var FormRenderer = BasicRenderer.extend({
* *
* For fields, it will return the $el of the field widget. Note that this * For fields, it will return the $el of the field widget. Note that this
* method is synchronous, field widgets are instantiated and appended, but * 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. * the _renderView method will properly wait.
* *
* @private * @private
@@ -705,7 +727,8 @@ var FormRenderer = BasicRenderer.extend({
if (node.attrs.class) { if (node.attrs.class) {
$result.addClass(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; return $result;
}, },
/** /**
@@ -870,7 +893,7 @@ var FormRenderer = BasicRenderer.extend({
_renderTagSheet: function (node) { _renderTagSheet: function (node) {
this.has_sheet = true; this.has_sheet = true;
var $sheet = $('<div>', {class: 'clearfix o_form_sheet'}); 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; return $sheet;
}, },
/** /**
@@ -885,12 +908,12 @@ var FormRenderer = BasicRenderer.extend({
}, },
/** /**
* Main entry point for the rendering. From here, we call _renderNode on * 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. * are done, it will resolves itself.
* *
* @private * @private
* @override method from BasicRenderer * @override method from BasicRenderer
* @returns {Deferred} * @returns {Promise}
*/ */
_renderView: function () { _renderView: function () {
var self = this; var self = this;
@@ -901,17 +924,17 @@ var FormRenderer = BasicRenderer.extend({
var $form = this._renderNode(this.arch).addClass(this.className); var $form = this._renderNode(this.arch).addClass(this.className);
delete this.defs; delete this.defs;
return $.when.apply($, defs).then(function () { return Promise.all(defs).then(function () {
self._updateView($form.contents()); self._updateView($form.contents());
if (self.state.res_id in self.alertFields) { if (self.state.res_id in self.alertFields) {
self.displayTranslationAlert(); self.displayTranslationAlert();
} }
}, function () {
$form.remove();
}).then(function(){ }).then(function(){
if (self.lastActivatedFieldIndex >= 0) { if (self.lastActivatedFieldIndex >= 0) {
self._activateNextFieldWidget(self.state, self.lastActivatedFieldIndex); self._activateNextFieldWidget(self.state, self.lastActivatedFieldIndex);
} }
}).guardedCatch(function () {
$form.remove();
}); });
}, },
/** /**
@@ -93,7 +93,7 @@ var FormView = BasicView.extend({
* *
* @private * @private
* @param {Widget} parent the parent of the model, if it has to be created * @param {Widget} parent the parent of the model, if it has to be created
* @returns {Deferred} * @returns {Promise}
*/ */
_loadSubviews: function (parent) { _loadSubviews: function (parent) {
var self = this; 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). * We set here the limit for the number of records fetched (in one page).
@@ -50,7 +50,7 @@ return AbstractModel.extend({
* @param {string[]} params.domain * @param {string[]} params.domain
* @param {Object} params.intervalMapping object linking fieldNames with intervals. * @param {Object} params.intervalMapping object linking fieldNames with intervals.
* this could be useful to simplify the code. For now this parameter is not used. * 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. * to keep track of various entities.
*/ */
load: function (params) { load: function (params) {
@@ -92,7 +92,7 @@ return AbstractModel.extend({
* @param {string[]} [params.groupBy] * @param {string[]} [params.groupBy]
* @param {string} [params.mode] one of 'bar', 'pie', 'line' * @param {string} [params.mode] one of 'bar', 'pie', 'line'
* @param {string} [params.measure] a valid field name * @param {string} [params.measure] a valid field name
* @returns {Deferred} * @returns {Promise}
*/ */
reload: function (handle, params) { reload: function (handle, params) {
if ('context' in params) { if ('context' in params) {
@@ -130,7 +130,7 @@ return AbstractModel.extend({
} }
if ('mode' in params) { if ('mode' in params) {
this.chart.mode = params.mode; this.chart.mode = params.mode;
return $.when(); return Promise.resolve();
} }
return this._loadGraph(); return this._loadGraph();
}, },
@@ -145,7 +145,7 @@ return AbstractModel.extend({
* in the field list, because they can be defined with an aggregation * in the field list, because they can be defined with an aggregation
* function, such as my_date:week * function, such as my_date:week
* *
* @returns {Deferred} * @returns {Promise}
*/ */
_loadGraph: function () { _loadGraph: function () {
var groupedBy = this.chart.groupedBy; var groupedBy = this.chart.groupedBy;
@@ -186,7 +186,7 @@ return AbstractModel.extend({
}).then(this._processData.bind(this, 'comparisonData'))); }).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 * Since read_group is insane and returns its result on different keys
@@ -99,7 +99,7 @@ return AbstractRenderer.extend({
* *
* @override * @override
* @private * @private
* @returns {Deferred} The _super deferred is actually resolved immediately * @returns {Promise} The _super promise is actually resolved immediately
*/ */
_render: function () { _render: function () {
if (this.to_remove) { if (this.to_remove) {
@@ -97,10 +97,7 @@ var KanbanColumn = Widget.extend({
this.$header = this.$('.o_kanban_header'); this.$header = this.$('.o_kanban_header');
for (var i = 0; i < this.data_records.length; i++) { for (var i = 0; i < this.data_records.length; i++) {
var def = this._addRecord(this.data_records[i]); defs.push(this._addRecord(this.data_records[i]));
if (def.state() === 'pending') {
defs.push(def);
}
} }
if (!config.device.isMobile) { if (!config.device.isMobile) {
@@ -170,7 +167,7 @@ var KanbanColumn = Widget.extend({
this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget: this})); 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 * 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. * Adds the quick create record to the top of the column.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
addQuickCreate: function () { addQuickCreate: function () {
if (this.folded) { if (this.folded) {
@@ -205,7 +202,7 @@ var KanbanColumn = Widget.extend({
} }
if (this.quickCreateWidget) { if (this.quickCreateWidget) {
return $.Deferred().reject(); return Promise.reject();
} }
this.trigger_up('close_quick_create'); // close other quick create widgets this.trigger_up('close_quick_create'); // close other quick create widgets
this.trigger_up('start_quick_create'); this.trigger_up('start_quick_create');
@@ -245,7 +242,7 @@ var KanbanColumn = Widget.extend({
* @param {Object} [options] * @param {Object} [options]
* @param {string} [options.position] * @param {string} [options.position]
* 'before' to add at the top, add at the bottom by default * 'before' to add at the top, add at the bottom by default
* @return {Deferred} * @return {Promise}
*/ */
_addRecord: function (recordState, options) { _addRecord: function (recordState, options) {
var record = new this.KanbanRecord(this, recordState, this.record_options); var record = new this.KanbanRecord(this, recordState, this.record_options);
@@ -76,6 +76,7 @@ var KanbanController = BasicController.extend({
/** /**
* @param {jQueryElement} $node * @param {jQueryElement} $node
* @returns {Promise}
*/ */
renderButtons: function ($node) { renderButtons: function ($node) {
if (this.hasButtons && this.is_action_enabled('create')) { 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('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this)); this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this));
this._updateButtons(); 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 * 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); params.domain = this.controlPanelDomain.concat(this.searchPanelDomain);
var superProm = this._super.apply(this, arguments); var superProm = this._super.apply(this, arguments);
var searchPanelProm = this._updateSearchPanel(); 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 // searchPanel has been re-rendered, so re-render the view
return self.renderer.render(); return self.renderer.render();
}); });
@@ -124,7 +126,7 @@ var KanbanController = BasicController.extend({
* @override method comes from field manager mixin * @override method comes from field manager mixin
* @private * @private
* @param {string} id local id from the basic record data * @param {string} id local id from the basic record data
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmSave: function (id) { _confirmSave: function (id) {
var data = this.model.get(this.handle, {raw: true}); var data = this.model.get(this.handle, {raw: true});
@@ -197,7 +199,7 @@ var KanbanController = BasicController.extend({
/** /**
* @param {number[]} ids * @param {number[]} ids
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_resequenceColumns: function (ids) { _resequenceColumns: function (ids) {
var state = this.model.get(this.handle, {raw: true}); var state = this.model.get(this.handle, {raw: true});
@@ -212,7 +214,7 @@ var KanbanController = BasicController.extend({
* @private * @private
* @param {string} column_id * @param {string} column_id
* @param {string[]} ids * @param {string[]} ids
* @returns {Deferred} * @returns {Promise}
*/ */
_resequenceRecords: function (column_id, ids) { _resequenceRecords: function (column_id, ids) {
var self = this; var self = this;
@@ -246,7 +248,7 @@ var KanbanController = BasicController.extend({
}, },
/** /**
* @private * @private
* @returns {$.Promise} * @returns {Promise}
*/ */
_updateSearchPanel: function () { _updateSearchPanel: function () {
return this._searchPanel.update({searchDomain: this.controlPanelDomain}); return this._searchPanel.update({searchDomain: this.controlPanelDomain});
@@ -296,7 +298,7 @@ var KanbanController = BasicController.extend({
self.renderer.updateColumn(db_id, data); self.renderer.updateColumn(db_id, data);
}); });
}); });
}).fail(this.reload.bind(this, {})); }).guardedCatch(this.reload.bind(this));
}, },
/** /**
* @private * @private
@@ -400,7 +402,7 @@ var KanbanController = BasicController.extend({
var relatedModelName = state.fields[state.groupedBy[0]].relation; var relatedModelName = state.fields[state.groupedBy[0]].relation;
this.model this.model
.deleteRecords([column.db_id], relatedModelName) .deleteRecords([column.db_id], relatedModelName)
.done(function () { .then(function () {
self.update({}, {reload: !column.isEmpty()}); self.update({}, {reload: !column.isEmpty()});
}); });
}, },
@@ -415,11 +417,12 @@ var KanbanController = BasicController.extend({
var self = this; var self = this;
this.model.loadColumnRecords(ev.data.columnID).then(function (dbID) { this.model.loadColumnRecords(ev.data.columnID).then(function (dbID) {
var data = self.model.get(dbID); var data = self.model.get(dbID);
self.renderer.updateColumn(dbID, data); return self.renderer.updateColumn(dbID, data).then(function() {
self._updateEnv(); self._updateEnv();
if (ev.data.onSuccess) { if (ev.data.onSuccess) {
ev.data.onSuccess(); ev.data.onSuccess();
} }
});
}); });
}, },
/** /**
@@ -470,8 +473,8 @@ var KanbanController = BasicController.extend({
this.model.createRecordInGroup(column.db_id, values) this.model.createRecordInGroup(column.db_id, values)
.then(update) .then(update)
.fail(function (error, ev) { .guardedCatch(function (reason) {
ev.preventDefault(); reason.event.preventDefault();
var columnState = self.model.get(column.db_id, {raw: true}); var columnState = self.model.get(column.db_id, {raw: true});
var context = columnState.getContext(); var context = columnState.getContext();
var state = self.model.get(self.handle, {raw: true}); var state = self.model.get(self.handle, {raw: true});
@@ -538,11 +541,15 @@ var KanbanController = BasicController.extend({
* *
* @private * @private
* @param {OdooEvent} ev * @param {OdooEvent} ev
* @param {function} [ev.data.onSuccess] callback to execute after applying
* changes
*/ */
_onUpdateRecord: function (ev) { _onUpdateRecord: function (ev) {
var onSuccess = ev.data.onSuccess;
delete ev.data.onSuccess;
var changes = _.clone(ev.data); var changes = _.clone(ev.data);
ev.data.force_save = true; 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. * Allow the user to archive/restore all the records of a column.
@@ -20,7 +20,7 @@ var KanbanModel = BasicModel.extend({
* *
* @param {string} groupID localID of the group * @param {string} groupID localID of the group
* @param {integer} resId id of the record * @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) { addRecordToGroup: function (groupID, resId) {
var self = this; var self = this;
@@ -52,7 +52,7 @@ var KanbanModel = BasicModel.extend({
* *
* @param {string} name * @param {string} name
* @param {string} parentID localID of the parent of the group * @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) { createGroup: function (name, parentID) {
var self = this; var self = this;
@@ -60,7 +60,7 @@ var KanbanModel = BasicModel.extend({
var groupBy = parent.groupedBy[0]; var groupBy = parent.groupedBy[0];
var groupByField = parent.fields[groupBy]; var groupByField = parent.fields[groupBy];
if (!groupByField || groupByField.type !== 'many2one') { 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({ return this._rpc({
model: groupByField.relation, model: groupByField.relation,
@@ -99,7 +99,7 @@ var KanbanModel = BasicModel.extend({
* *
* @param {string} groupID * @param {string} groupID
* @param {Object} values * @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) { createRecordInGroup: function (groupID, values) {
var self = this; var self = this;
@@ -187,7 +187,7 @@ var KanbanModel = BasicModel.extend({
* Opens a given group and loads its <limit> first records * Opens a given group and loads its <limit> first records
* *
* @param {string} groupID * @param {string} groupID
* @returns {Deferred} * @returns {Promise}
*/ */
loadColumnRecords: function (groupID) { loadColumnRecords: function (groupID) {
var dataPoint = this.localData[groupID]; var dataPoint = this.localData[groupID];
@@ -198,7 +198,7 @@ var KanbanModel = BasicModel.extend({
* Load more records in a group. * Load more records in a group.
* *
* @param {string} groupID localID of the 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) { loadMore: function (groupID) {
var group = this.localData[groupID]; var group = this.localData[groupID];
@@ -213,7 +213,7 @@ var KanbanModel = BasicModel.extend({
* @param {string} recordID localID of the record * @param {string} recordID localID of the record
* @param {string} groupID localID of the new group of the record * @param {string} groupID localID of the new group of the record
* @param {string} parentID localID of the parent * @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) { moveRecord: function (recordID, groupID, parentID) {
var self = this; var self = this;
@@ -330,7 +330,7 @@ var KanbanModel = BasicModel.extend({
/** /**
* @private * @private
* @param {Object} dataPoint * @param {Object} dataPoint
* @returns {Deferred<Object>} * @returns {Promise<Object>}
*/ */
_readProgressBarGroup: function (list, options) { _readProgressBarGroup: function (list, options) {
var self = this; var self = this;
@@ -345,7 +345,8 @@ var KanbanModel = BasicModel.extend({
context: list.context, 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) { _.each(list.data, function (groupID) {
var group = self.localData[groupID]; var group = self.localData[groupID];
group.progressBarValues = _.extend({ group.progressBarValues = _.extend({
@@ -365,13 +366,13 @@ var KanbanModel = BasicModel.extend({
* *
* @private * @private
* @param {Object} list a list of groups * @param {Object} list a list of groups
* @returns {Deferred} * @returns {Promise}
*/ */
_readTooltipFields: function (list) { _readTooltipFields: function (list) {
var self = this; var self = this;
var groupedByField = list.fields[list.groupedBy[0].split(':')[0]]; var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') { if (groupedByField.type !== 'many2one') {
return $.when(); return Promise.resolve();
} }
var groupIds = _.reduce(list.data, function (groupIds, id) { var groupIds = _.reduce(list.data, function (groupIds, id) {
var res_id = self.get(id, {raw: true}).res_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 * Reloads all progressbar data. This is done after given promise and
* insures that the given deferred's result is not lost. * insures that the given promise's result is not lost.
* *
* @private * @private
* @param {string} recordID * @param {string} recordID
* @param {Deferred} def * @param {Promise} def
* @returns {Deferred} * @returns {Promise}
*/ */
_reloadProgressBarGroupFromRecord: function (recordID, def) { _reloadProgressBarGroupFromRecord: function (recordID, def) {
var element = this.localData[recordID]; var element = this.localData[recordID];
@@ -62,7 +62,7 @@ var KanbanRecord = Widget.extend({
* @override * @override
*/ */
start: function () { 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. * 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 * Re-renders the record with a new state
* *
* @param {Object} state * @param {Object} state
* @returns {Deferred} * @returns {Promise}
*/ */
update: function (state) { update: function (state) {
// detach the widgets because the record will empty its $el, which will // detach the widgets because the record will empty its $el, which will
@@ -147,7 +147,7 @@ var KanbanRecord = Widget.extend({
} }
if (typeof variable === 'string') { if (typeof variable === 'string') {
var index = 0; var index = 0;
for (var i = 0 ; i < variable.length ; i++) { for (var i = 0; i < variable.length; i++) {
index += variable.charCodeAt(i); index += variable.charCodeAt(i);
} }
return index % NB_KANBAN_RECORD_COLORS; return index % NB_KANBAN_RECORD_COLORS;
@@ -195,7 +195,7 @@ var KanbanRecord = Widget.extend({
field = options.preview_image; field = options.preview_image;
var unique = this.record.__last_update && this.record.__last_update.value.replace(/[^0-9]/g, ''); var unique = this.record.__last_update && this.record.__last_update.value.replace(/[^0-9]/g, '');
var session = this.getSession(); 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) { if (cache !== undefined) {
// Set the cache duration in seconds. // Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10); url += '&cache=' + parseInt(cache, 10);
@@ -300,6 +300,7 @@ var KanbanRecord = Widget.extend({
// field's widgets point of view // field's widgets point of view
// that dict being shared between records, we don't modify it // that dict being shared between records, we don't modify it
// in place // in place
var self = this;
var attrs = Object.create(null); var attrs = Object.create(null);
_.each(this.fieldsInfo[field_name], function (value, key) { _.each(this.fieldsInfo[field_name], function (value, key) {
if (_.str.startsWith(key, 't-att-')) { if (_.str.startsWith(key, 't-att-')) {
@@ -308,13 +309,13 @@ var KanbanRecord = Widget.extend({
} }
attrs[key] = value; 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 widget = new Widget(this, field_name, this.state, options);
var def = widget.replace($field); var def = widget.replace($field);
if (def.state() === 'pending') { this.defs.push(def);
this.defs.push(def); def.then(function () {
} self._setFieldDisplay(widget.$el, field_name);
this._setFieldDisplay(widget.$el, field_name); });
return widget; return widget;
}, },
_processWidgets: function () { _processWidgets: function () {
@@ -324,23 +325,23 @@ var KanbanRecord = Widget.extend({
var Widget = widgetRegistry.get($field.attr('name')); var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state); var widget = new Widget(self, self.state);
var def = widget._widgetRenderAndInsert(function () {}); var def = widget._widgetRenderAndInsert(function () { });
if (def.state() === 'pending') { self.defs.push(def);
self.defs.push(def); def.then(function () {
} $field.replaceWith(widget.$el);
widget.$el.addClass('o_widget'); widget.$el.addClass('o_widget');
$field.replaceWith(widget.$el); });
}); });
}, },
/** /**
* Renders the record * Renders the record
* *
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
this.defs = []; this.defs = [];
this._replaceElement(this.qweb.render('kanban-box', this.qweb_context)); 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.attr('role', 'article');
this.$el.data('record', this); this.$el.data('record', this);
if (this.$el.hasClass('oe_kanban_global_click') || if (this.$el.hasClass('oe_kanban_global_click') ||
@@ -357,9 +358,9 @@ var KanbanRecord = Widget.extend({
this._attachTooltip(); this._attachTooltip();
// We use boostrap tooltips for better and faster display // 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 * 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 colorHelp = _.str.sprintf(_t("Card color: %s"), this._getColorname(this.recordData[color_field]));
var colorClass = this._getColorClassname(this.recordData[color_field]); var colorClass = this._getColorClassname(this.recordData[color_field]);
this.$el.addClass(colorClass); 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(); r.raw_value = value.toDate();
} else if (r.type === 'one2many' || r.type === 'many2many') { } else if (r.type === 'one2many' || r.type === 'many2many') {
r.raw_value = value.count ? value.res_ids : []; 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; r.raw_value = value && value.res_id || false;
} else { } else {
r.raw_value = value; r.raw_value = value;
@@ -554,13 +555,13 @@ var KanbanRecord = Widget.extend({
switch (type) { switch (type) {
case 'edit': 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; break;
case 'open': case 'open':
this.trigger_up('open_record', {id: this.db_id}); this.trigger_up('open_record', { id: this.db_id });
break; break;
case 'delete': 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; break;
case 'action': case 'action':
case 'object': case 'object':
@@ -47,14 +47,14 @@ var RecordQuickCreate = Widget.extend({
*/ */
willStart: function () { willStart: function () {
var self = this; var self = this;
var def1 = this._super.apply(this, arguments); var superWillStart = this._super.apply(this, arguments);
var def2; var viewsLoaded;
if (this.formViewRef) { if (this.formViewRef) {
var views = [[false, 'form']]; var views = [[false, 'form']];
var context = _.extend({}, this.context, { var context = _.extend({}, this.context, {
form_view_ref: this.formViewRef, form_view_ref: this.formViewRef,
}); });
def2 = this.loadViews(this.model, context, views); viewsLoaded = this.loadViews(this.model, context, views);
} else { } else {
var fieldsView = {}; var fieldsView = {};
fieldsView.arch = '<form>' + fieldsView.arch = '<form>' +
@@ -65,9 +65,9 @@ var RecordQuickCreate = Widget.extend({
}; };
fieldsView.fields = fields; fieldsView.fields = fields;
fieldsView.viewFields = 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, { var formView = new QuickCreateFormView(fieldsViews.form, {
context: self.context, context: self.context,
modelName: self.model, modelName: self.model,
@@ -78,7 +78,7 @@ var RecordQuickCreate = Widget.extend({
return self.controller.appendTo(document.createDocumentFragment()); return self.controller.appendTo(document.createDocumentFragment());
}); });
}); });
return $.when(def1, def2); return Promise.all([superWillStart, viewsLoaded]);
}, },
/** /**
* @override * @override
@@ -113,7 +113,7 @@ var RecordQuickCreate = Widget.extend({
* have been made yet * have been made yet
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
cancel: function () { cancel: function () {
var self = this; var self = this;
@@ -129,7 +129,6 @@ var RecordQuickCreate = Widget.extend({
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/** /**
* @override
* @private * @private
* @param {Object} [options] * @param {Object} [options]
* @param {boolean} [options.openRecord] set to true to directly open the * @param {boolean} [options.openRecord] set to true to directly open the
@@ -139,7 +138,7 @@ var RecordQuickCreate = Widget.extend({
var self = this; var self = this;
if (this._disabled) { if (this._disabled) {
// don't do anything if we are already creating a record // 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 // disable the widget to prevent the user from creating multiple records
// with the current values ; if the create works, the widget will be // with the current values ; if the create works, the widget will be
@@ -157,13 +156,13 @@ var RecordQuickCreate = Widget.extend({
} else { } else {
self._enableQuickCreate(); self._enableQuickCreate();
} }
}).fail(this._enableQuickCreate.bind(this)); }).guardedCatch(this._enableQuickCreate.bind(this));
}, },
/** /**
* Notifies the environment that the quick creation must be cancelled * Notifies the environment that the quick creation must be cancelled
* *
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_cancel: function () { _cancel: function () {
this.trigger_up('cancel_quick_create'); this.trigger_up('cancel_quick_create');
@@ -148,7 +148,7 @@ var KanbanRenderer = BasicRenderer.extend({
/** /**
* Displays the quick create record in the first column. * Displays the quick create record in the first column.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
addQuickCreate: function () { addQuickCreate: function () {
return this.widgets[0].addQuickCreate(); return this.widgets[0].addQuickCreate();
@@ -187,7 +187,7 @@ var KanbanRenderer = BasicRenderer.extend({
* @param {boolean} [options.openQuickCreate] if true, directly opens the * @param {boolean} [options.openQuickCreate] if true, directly opens the
* QuickCreate widget in the updated column * QuickCreate widget in the updated column
* *
* @returns {Deferred} * @returns {Promise}
*/ */
updateColumn: function (localID, columnState, options) { updateColumn: function (localID, columnState, options) {
var self = this; var self = this;
@@ -204,7 +204,7 @@ var KanbanRenderer = BasicRenderer.extend({
if (options && options.openQuickCreate) { if (options && options.openQuickCreate) {
def = newColumn.addQuickCreate(); def = newColumn.addQuickCreate();
} }
return $.when(def).then(function () { return Promise.resolve(def).then(function () {
newColumn.$el.insertAfter(column.$el); newColumn.$el.insertAfter(column.$el);
self._toggleNoContentHelper(); self._toggleNoContentHelper();
// When a record has been quick created, the new column directly // 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. * Updates a given record with its new state.
* *
* @param {Object} recordState * @param {Object} recordState
* @returns {Deferred} * @returns {Promise}
*/ */
updateRecord: function (recordState) { updateRecord: function (recordState) {
var isGrouped = !!this.state.groupedBy.length; var isGrouped = !!this.state.groupedBy.length;
@@ -242,7 +242,7 @@ var KanbanRenderer = BasicRenderer.extend({
if (record) { if (record) {
return record.update(recordState); return record.update(recordState);
} }
return $.when(); return Promise.resolve();
}, },
/** /**
* @override * @override
@@ -295,10 +295,13 @@ var KanbanRenderer = BasicRenderer.extend({
* @param {integer} nbDivs the number of divs to append * @param {integer} nbDivs the number of divs to append
*/ */
_renderGhostDivs: function (fragment, nbDivs) { _renderGhostDivs: function (fragment, nbDivs) {
var ghostDefs = [];
for (var $ghost, i = 0; i < nbDivs; i++) { for (var $ghost, i = 0; i < nbDivs; i++) {
$ghost = $('<div>').addClass('o_kanban_record o_kanban_ghost'); $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. * Renders an grouped kanban view in a fragment.
@@ -321,10 +324,7 @@ var KanbanRenderer = BasicRenderer.extend({
def = column.appendTo(fragment); def = column.appendTo(fragment);
self.widgets.push(column); self.widgets.push(column);
} }
if (def.state() === 'pending') { self.defs.push(def);
self.defs.push(def);
}
}); });
// remove previous sorting // remove previous sorting
@@ -359,13 +359,13 @@ var KanbanRenderer = BasicRenderer.extend({
this.quickCreate = new ColumnQuickCreate(this, { this.quickCreate = new ColumnQuickCreate(this, {
examples: this.examples, 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 // Open it directly if there is no column yet
if (!self.state.data.length) { if (!self.state.data.length) {
self.quickCreate.toggleFold(); self.quickCreate.toggleFold();
self._renderExampleBackground(fragment); self._renderExampleBackground(fragment);
} }
}); }));
} }
} }
}, },
@@ -382,13 +382,14 @@ var KanbanRenderer = BasicRenderer.extend({
var kanbanRecord = new KanbanRecord(self, record, self.recordOptions); var kanbanRecord = new KanbanRecord(self, record, self.recordOptions);
self.widgets.push(kanbanRecord); self.widgets.push(kanbanRecord);
var def = kanbanRecord.appendTo(fragment); 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 // 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 * @override
@@ -398,28 +399,31 @@ var KanbanRenderer = BasicRenderer.extend({
var self = this; var self = this;
var oldWidgets = this.widgets; var oldWidgets = this.widgets;
this.widgets = []; this.widgets = [];
this.$el.empty();
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 // render the kanban view
this.defs = []; var isGrouped = !!this.state.groupedBy.length;
var fragment = document.createDocumentFragment();
var defs = [];
this.defs = defs;
if (isGrouped) { if (isGrouped) {
this._renderGrouped(fragment); this._renderGrouped(fragment);
} else { } else {
this._renderUngrouped(fragment); this._renderUngrouped(fragment);
} }
this.$el.append(fragment); delete this.defs;
this._toggleNoContentHelper();
var defs = this.defs;
return this._super.apply(this, arguments).then(function () { return this._super.apply(this, arguments).then(function () {
_.invoke(oldWidgets, 'destroy'); return Promise.all(defs).then(function () {
if (self._isInDom) { self.$el.empty();
_.invoke(self.widgets, 'on_attach_callback'); self.$el.toggleClass('o_kanban_grouped', isGrouped);
} self.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
return $.when.apply(null, defs); self.$el.append(fragment);
self._toggleNoContentHelper();
_.invoke(oldWidgets, 'destroy');
if (self._isInDom) {
_.invoke(self.widgets, 'on_attach_callback');
}
});
}); });
}, },
/** /**
@@ -75,7 +75,7 @@ KanbanRenderer.include({
/** /**
* Displays the quick create record in the active column * Displays the quick create record in the active column
* *
* @returns {Deferred} * @returns {Promise}
*/ */
addQuickCreate: function () { addQuickCreate: function () {
return this.widgets[this.activeColumnIndex].addQuickCreate(); return this.widgets[this.activeColumnIndex].addQuickCreate();
@@ -260,26 +260,26 @@ KanbanRenderer.include({
* @private * @private
* @param {integer} moveToIndex index of the column to move to * @param {integer} moveToIndex index of the column to move to
* @param {boolean} [animate=false] set to true to animate * @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 * and displayed
*/ */
_moveToGroup: function (moveToIndex, animate) { _moveToGroup: function (moveToIndex, animate) {
var self = this; var self = this;
if (moveToIndex < 0 || moveToIndex >= this.widgets.length) { if (moveToIndex < 0 || moveToIndex >= this.widgets.length) {
this._layoutUpdate(animate); this._layoutUpdate(animate);
return $.when(); return Promise.resolve();
} }
var def = $.Deferred();
this.activeColumnIndex = moveToIndex; this.activeColumnIndex = moveToIndex;
var column = this.widgets[this.activeColumnIndex]; var column = this.widgets[this.activeColumnIndex];
this.trigger_up('kanban_load_records', { return new Promise(function (resolve) {
columnID: column.db_id, self.trigger_up('kanban_load_records', {
onSuccess: function () { columnID: column.db_id,
self._layoutUpdate(animate); onSuccess: function () {
def.resolve(); self._layoutUpdate(animate);
}, resolve();
},
});
}); });
return def;
}, },
/** /**
@@ -287,22 +287,23 @@ KanbanRenderer.include({
* @private * @private
*/ */
_renderGrouped: function (fragment) { _renderGrouped: function (fragment) {
var result = this._super.apply(this, arguments); var self = this;
var data = []; this._super.apply(this, arguments);
_.each(this.state.data, function (group) { this.defs.push(Promise.all(this.defs).then(function () {
if (!group.value) { var data = [];
group = _.extend({}, group, {value: _t('Undefined')}); _.each(self.state.data, function (group) {
data.unshift(group); if (!group.value) {
} group = _.extend({}, group, {value: _t('Undefined')});
else { data.unshift(group);
data.push(group); } else {
} data.push(group);
}); }
});
$(qweb.render('KanbanView.MobileTabs', { $(qweb.render('KanbanView.MobileTabs', {
data: data, data: data,
})).prependTo(fragment); })).prependTo(fragment);
return result; }));
}, },
/** /**
@@ -139,7 +139,7 @@ var KanbanView = BasicView.extend({
/** /**
* @private * @private
* @param {Widget} parent * @param {Widget} parent
* @returns {$.Promise} resolved when the searchPanel is ready * @returns {Promise} resolved when the searchPanel is ready
*/ */
_createSearchPanel: function (parent) { _createSearchPanel: function (parent) {
var self = this; var self = this;
@@ -71,7 +71,7 @@ var QuickCreateFormController = FormController.extend({
*/ */
commitChanges: function () { commitChanges: function () {
var mutexDef = this.mutex.getUnlockedDef(); 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 * @returns {Object} the changes done on the current record
@@ -55,7 +55,7 @@ var SearchPanel = Widget.extend({
var loadProm = this._fetchCategories().then(function () { var loadProm = this._fetchCategories().then(function () {
return self._fetchFilters().then(self._applyDefaultFilterValues.bind(self)); 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 * @override
@@ -82,18 +82,18 @@ var SearchPanel = Widget.extend({
* *
* @param {Object} params * @param {Object} params
* @param {Array[]} params.searchDomain domain coming from controlPanel * @param {Array[]} params.searchDomain domain coming from controlPanel
* @returns {$.Promise} * @returns {Promise}
*/ */
update: function (params) { update: function (params) {
var currentSearchDomainStr = JSON.stringify(this.searchDomain); var currentSearchDomainStr = JSON.stringify(this.searchDomain);
var newSearchDomainStr = JSON.stringify(params.searchDomain); var newSearchDomainStr = JSON.stringify(params.searchDomain);
var def; var filtersProm;
if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) { if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) {
this.needReload = false; this.needReload = false;
this.searchDomain = params.searchDomain; 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. * Fetch values for each category. This is done only once, at startup.
* *
* @private * @private
* @returns {$.Promise} resolved when all categories have been fetched * @returns {Promise} resolved when all categories have been fetched
*/ */
_fetchCategories: function () { _fetchCategories: function () {
var self = this; 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 category = self.categories[categoryId];
var field = self.fields[category.fieldName]; var field = self.fields[category.fieldName];
var def; var categoriesProm;
if (field.type === 'selection') { if (field.type === 'selection') {
var values = field.selection.map(function (value) { var values = field.selection.map(function (value) {
return {id: value[0], display_name: value[1]}; return {id: value[0], display_name: value[1]};
}); });
def = $.when(values); categoriesProm = Promise.resolve(values);
} else { } else {
def = self._rpc({ categoriesProm = self._rpc({
method: 'search_panel_select_range', method: 'search_panel_select_range',
model: self.model, model: self.model,
args: [category.fieldName], args: [category.fieldName],
@@ -254,18 +254,18 @@ var SearchPanel = Widget.extend({
return result.values; return result.values;
}); });
} }
return def.then(function (values) { return categoriesProm.then(function (values) {
self._createCategoryTree(categoryId, 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 * Fetch values for each filter. This is done at startup, and at each reload
* (when the controlPanel or searchPanel domain changes). * (when the controlPanel or searchPanel domain changes).
* *
* @private * @private
* @returns {$.Promise} resolved when all filters have been fetched * @returns {Promise} resolved when all filters have been fetched
*/ */
_fetchFilters: function () { _fetchFilters: function () {
var self = this; var self = this;
@@ -276,7 +276,7 @@ var SearchPanel = Widget.extend({
}); });
var categoryDomain = this._getCategoryDomain(); var categoryDomain = this._getCategoryDomain();
var filterDomain = this._getFilterDomain(); 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]; var filter = self.filters[filterId];
return self._rpc({ return self._rpc({
method: 'search_panel_select_multi_range', method: 'search_panel_select_multi_range',
@@ -294,7 +294,7 @@ var SearchPanel = Widget.extend({
self._createFilterTree(filterId, values); self._createFilterTree(filterId, values);
}); });
}); });
return $.when.apply($, defs); return Promise.all(proms);
}, },
/** /**
* Compute and return the domain based on the current active categories. * Compute and return the domain based on the current active categories.
@@ -65,7 +65,7 @@ var ListController = BasicController.extend({
* this method should be private, most of the code in the sidebar should be * 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... * 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 () { getActiveDomain: function () {
// TODO: this method should be synchronous... // TODO: this method should be synchronous...
@@ -73,9 +73,9 @@ var ListController = BasicController.extend({
if (this.$('thead .o_list_record_selector input').prop('checked')) { if (this.$('thead .o_list_record_selector input').prop('checked')) {
var searchQuery = this._controlPanel ? this._controlPanel.getSearchQuery() : {}; var searchQuery = this._controlPanel ? this._controlPanel.getSearchQuery() : {};
var record = self.model.get(self.handle, {raw: true}); 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 { } else {
return $.Deferred().resolve(); return Promise.resolve();
} }
}, },
/* /*
@@ -143,6 +143,7 @@ var ListController = BasicController.extend({
* main buttons) * main buttons)
* *
* @param {jQuery Node} $node * @param {jQuery Node} $node
* @returns {Promise}
*/ */
renderSidebar: function ($node) { renderSidebar: function ($node) {
var self = this; var self = this;
@@ -180,10 +181,11 @@ var ListController = BasicController.extend({
}, },
actions: _.extend(this.toolbarActions, {other: other}), actions: _.extend(this.toolbarActions, {other: other}),
}); });
this.sidebar.appendTo($node); return this.sidebar.appendTo($node).then(function() {
self._toggleSidebar();
this._toggleSidebar(); });
} }
return Promise.resolve();
}, },
/** /**
* Overrides to update the list of selected records * Overrides to update the list of selected records
@@ -243,10 +245,11 @@ var ListController = BasicController.extend({
}); });
}).then(function (recordID) { }).then(function (recordID) {
var state = self.model.get(self.handle); var state = self.model.get(self.handle);
self.renderer.updateState(state, {}); self.renderer.updateState(state, {})
self.renderer.editRecord(recordID); .then(function () {
self._updatePager(); self.renderer.editRecord(recordID);
}).always(this._enableButtons.bind(this)); }).then(self._updatePager.bind(self));
}).then(this._enableButtons.bind(this)).guardedCatch(this._enableButtons.bind(this));
}, },
/** /**
* Archive the current selection * Archive the current selection
@@ -254,11 +257,11 @@ var ListController = BasicController.extend({
* @private * @private
* @param {string[]} ids * @param {string[]} ids
* @param {boolean} archive * @param {boolean} archive
* @returns {Deferred} * @returns {Promise}
*/ */
_archive: function (ids, archive) { _archive: function (ids, archive) {
if (ids.length === 0) { if (ids.length === 0) {
return $.when(); return Promise.resolve();
} }
return this.model return this.model
.toggleActive(ids, !archive, this.handle) .toggleActive(ids, !archive, this.handle)
@@ -298,7 +301,7 @@ var ListController = BasicController.extend({
* @override * @override
* @param {string} id a basicmodel valid resource handle. It is supposed to * @param {string} id a basicmodel valid resource handle. It is supposed to
* be a record from the list view. * be a record from the list view.
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmSave: function (id) { _confirmSave: function (id) {
var state = this.model.get(this.handle); var state = this.model.get(this.handle);
@@ -313,13 +316,13 @@ var ListController = BasicController.extend({
* @override * @override
* @private * @private
* @param {string} [recordID] - default to main recordID * @param {string} [recordID] - default to main recordID
* @returns {Deferred} * @returns {Promise}
*/ */
_discardChanges: function (recordID) { _discardChanges: function (recordID) {
if ((recordID || this.handle) === this.handle) { if ((recordID || this.handle) === this.handle) {
recordID = this.renderer.getEditableRecordID(); recordID = this.renderer.getEditableRecordID();
if (recordID === null) { if (recordID === null) {
return $.when(); return Promise.resolve();
} }
} }
var self = this; var self = this;
@@ -343,7 +346,7 @@ var ListController = BasicController.extend({
* @private * @private
* @param {string} mode * @param {string} mode
* @param {string} [recordID] - default to main recordID * @param {string} [recordID] - default to main recordID
* @returns {Deferred} * @returns {Promise}
*/ */
_setMode: function (mode, recordID) { _setMode: function (mode, recordID) {
if ((recordID || this.handle) !== this.handle) { if ((recordID || this.handle) !== this.handle) {
@@ -373,11 +376,13 @@ var ListController = BasicController.extend({
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
_update: function () { _update: function () {
this._toggleSidebar(); var self = this;
return this._super.apply(this, arguments); return this._super.apply(this, arguments).then(function() {
self._toggleSidebar();
});
}, },
/** /**
* This helper simply makes sure that the control panel buttons matches the * 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 record = self.model.get(self.handle);
var editedRecord = record.data[ev.data.index]; var editedRecord = record.data[ev.data.index];
self._setMode('edit', editedRecord.id) self._setMode('edit', editedRecord.id)
.done(ev.data.onSuccess); .then(ev.data.onSuccess);
}, },
}); });
}, },
@@ -523,8 +528,8 @@ var ListController = BasicController.extend({
_onSaveLine: function (ev) { _onSaveLine: function (ev) {
var recordID = ev.data.recordID; var recordID = ev.data.recordID;
this.saveRecord(recordID) this.saveRecord(recordID)
.done(ev.data.onSuccess) .then(ev.data.onSuccess)
.fail(ev.data.onFailure); .guardedCatch(ev.data.onFailure);
}, },
/** /**
* When the current selection changes (by clicking on the checkboxes on the * When the current selection changes (by clicking on the checkboxes on the
@@ -93,7 +93,7 @@ ListRenderer.include({
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
// deliberately use the 'editable' attribute instead of '_isEditable' // deliberately use the 'editable' attribute instead of '_isEditable'
@@ -171,7 +171,7 @@ ListRenderer.include({
* @param {string} id * @param {string} id
* @param {string[]} fields * @param {string[]} fields
* @param {OdooEvent} ev * @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 * that have been reset
*/ */
confirmUpdate: function (state, id, fields, ev) { confirmUpdate: function (state, id, fields, ev) {
@@ -180,7 +180,7 @@ ListRenderer.include({
// store the cursor position to restore it once potential onchanges have // store the cursor position to restore it once potential onchanges have
// been applied // been applied
var currentRowID, currentWidget, focusedElement, selectionRange; var currentRowID, currentWidget, focusedElement, selectionRange;
if (self.currentRow !== null) { if (this.currentRow !== null) {
currentRowID = this.state.data[this.currentRow].id; currentRowID = this.state.data[this.currentRow].id;
currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex]; currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex];
if (currentWidget) { if (currentWidget) {
@@ -212,10 +212,14 @@ ListRenderer.include({
}); });
var newRowIndex = _.findIndex(state.data, {id: id}); var newRowIndex = _.findIndex(state.data, {id: id});
var $lastRow = $row; var $lastRow = $row;
_.each(state.data, function (record, index) { var defs = [];
self.defs = defs;
state.data.forEach(function (record, index) {
if (index === newRowIndex) { if (index === newRowIndex) {
return; 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); var $newRow = self._renderRow(record);
if (index < newRowIndex) { if (index < newRowIndex) {
$newRow.insertBefore($row); $newRow.insertBefore($row);
@@ -224,20 +228,23 @@ ListRenderer.include({
$lastRow = $newRow; $lastRow = $newRow;
} }
}); });
if (self.currentRow !== null) { delete self.defs;
self.currentRow = newRowIndex; return Promise.all(defs).then(function () {
return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true}).then(function () { if (self.currentRow !== null) {
// restore the cursor position self.currentRow = newRowIndex;
currentRowID = self.state.data[newRowIndex].id; return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true});
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex]; }
if (currentWidget) { }).then(function () {
focusedElement = currentWidget.getFocusableElement().get(0); // restore the cursor position
if (selectionRange) { currentRowID = self.state.data[newRowIndex].id;
dom.setSelectionRange(focusedElement, selectionRange); 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} recordID
* @param {string} mode * @param {string} mode
* @returns {Deferred} * @returns {Promise}
*/ */
setRowMode: function (recordID, mode) { setRowMode: function (recordID, mode) {
var self = this; var self = this;
@@ -321,7 +328,7 @@ ListRenderer.include({
} }
if (rowIndex < 0) { if (rowIndex < 0) {
return $.when(); return Promise.resolve();
} }
var editMode = (mode === 'edit'); var editMode = (mode === 'edit');
@@ -350,7 +357,7 @@ ListRenderer.include({
options.mode = editMode ? 'edit' : 'readonly'; options.mode = editMode ? 'edit' : 'readonly';
// Switch each cell to the new mode; note: the '_renderBodyCell' // 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. // so we create the array and delete it after the rendering.
var defs = []; var defs = [];
this.defs = defs; this.defs = defs;
@@ -385,7 +392,7 @@ ListRenderer.include({
$row.toggleClass('o_selected_row', editMode); $row.toggleClass('o_selected_row', editMode);
$row.find('.o_list_record_selector input').prop('disabled', !record.res_id); $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 // necessary to trigger resize on fieldtexts
core.bus.trigger('DOM_updated'); core.bus.trigger('DOM_updated');
}); });
@@ -399,27 +406,32 @@ ListRenderer.include({
* prevent subsequent editions. These edits would be lost, because the list * prevent subsequent editions. These edits would be lost, because the list
* view only saves records when unselecting a row. * 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 * possibly removed). If may be rejected, when the row is dirty and the
* user refuses to discard its changes. * user refuses to discard its changes.
*/ */
unselectRow: function () { unselectRow: function () {
var self = this;
// Protect against calling this method when no row is selected // Protect against calling this method when no row is selected
if (this.currentRow === null) { if (this.currentRow === null) {
return $.when(); return Promise.resolve();
} }
var record = this.state.data[this.currentRow]; var record = this.state.data[this.currentRow];
var recordWidgets = this.allFieldWidgets[record.id]; var recordWidgets = this.allFieldWidgets[record.id];
toggleWidgets(true); toggleWidgets(true);
var def = $.Deferred(); var prom = new Promise(function (resolve, reject) {
this.trigger_up('save_line', { self.trigger_up('save_line', {
recordID: record.id, recordID: record.id,
onSuccess: def.resolve.bind(def), onSuccess: resolve,
onFailure: def.reject.bind(def), onFailure: reject,
});
}); });
return def.fail(toggleWidgets.bind(null, false)); prom.guardedCatch(function() {
toggleWidgets(false);
});
return prom;
function toggleWidgets(disabled) { function toggleWidgets(disabled) {
_.each(recordWidgets, function (widget) { _.each(recordWidgets, function (widget) {
@@ -511,7 +523,7 @@ ListRenderer.include({
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
this.currentRow = null; this.currentRow = null;
@@ -524,10 +536,9 @@ ListRenderer.include({
* widget. * widget.
* *
* @override * @override
* @returns {jQueryElement}
*/ */
_renderBody: function () { _renderBody: function () {
var $body = this._super(); var $body = this._super.apply(this, arguments);
if (this.hasHandle) { if (this.hasHandle) {
$body.sortable({ $body.sortable({
axis: 'y', axis: 'y',
@@ -568,15 +579,13 @@ ListRenderer.include({
* on the first real column. * on the first real column.
* *
* @override * @override
* @returns {jQueryElement} * @returns {jQueryElement[]}
*/ */
_renderRows: function () { _renderRows: function () {
var self = this;
var $rows = this._super(); var $rows = this._super();
if (this.addCreateLine) { if (this.addCreateLine) {
var $tr = $('<tr>'); var $tr = $('<tr>');
var colspan = self._getNumberOfCols(); var colspan = this._getNumberOfCols();
if (this.handleField) { if (this.handleField) {
colspan = colspan - 1; colspan = colspan - 1;
@@ -589,7 +598,7 @@ ListRenderer.include({
$tr.append($td); $tr.append($td);
$rows.push($tr); $rows.push($tr);
_.each(self.creates, function (create, index) { _.each(this.creates, function (create, index) {
var $a = $('<a href="#" role="button">') var $a = $('<a href="#" role="button">')
.attr('data-context', create.context) .attr('data-context', create.context)
.text(create.string); .text(create.string);
@@ -604,7 +613,7 @@ ListRenderer.include({
/** /**
* @override * @override
* @private * @private
* @returns {Deferred} this deferred is resolved immediately * @returns {Promise} this promise is resolved immediately
*/ */
_renderView: function () { _renderView: function () {
var self = this; var self = this;
@@ -692,13 +701,13 @@ ListRenderer.include({
* @param {boolean} [options.force=false] if true, force selecting the cell * @param {boolean} [options.force=false] if true, force selecting the cell
* even if seems to be already the selected one (useful after a re- * even if seems to be already the selected one (useful after a re-
* rendering, to reset the focus on the correct field) * 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) { _selectCell: function (rowIndex, fieldIndex, options) {
options = options || {}; options = options || {};
// Do nothing if the user tries to select current cell // Do nothing if the user tries to select current cell
if (!options.force && rowIndex === this.currentRow && fieldIndex === this.currentFieldIndex) { if (!options.force && rowIndex === this.currentRow && fieldIndex === this.currentFieldIndex) {
return $.when(); return Promise.resolve();
} }
var wrap = options.wrap === undefined ? true : options.wrap; var wrap = options.wrap === undefined ? true : options.wrap;
@@ -707,7 +716,7 @@ ListRenderer.include({
return this._selectRow(rowIndex).then(function () { return this._selectRow(rowIndex).then(function () {
var record = self.state.data[rowIndex]; var record = self.state.data[rowIndex];
if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) { if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) {
return $.Deferred().reject(); return Promise.reject();
} }
// _activateFieldWidget might trigger an onchange, // _activateFieldWidget might trigger an onchange,
// which requires currentFieldIndex to be set // which requires currentFieldIndex to be set
@@ -721,7 +730,7 @@ ListRenderer.include({
}); });
if (fieldIndex < 0) { if (fieldIndex < 0) {
self.currentFieldIndex = oldFieldIndex; self.currentFieldIndex = oldFieldIndex;
return $.Deferred().reject(); return Promise.reject();
} }
self.currentFieldIndex = fieldIndex; self.currentFieldIndex = fieldIndex;
}); });
@@ -730,12 +739,12 @@ ListRenderer.include({
* Activates the row at the given row index. * Activates the row at the given row index.
* *
* @param {integer} rowIndex * @param {integer} rowIndex
* @returns {Deferred} * @returns {Promise}
*/ */
_selectRow: function (rowIndex) { _selectRow: function (rowIndex) {
// Do nothing if already selected // Do nothing if already selected
if (rowIndex === this.currentRow) { if (rowIndex === this.currentRow) {
return $.when(); return Promise.resolve();
} }
// To select a row, the currently selected one must be unselected first // 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 // The row to selected doesn't exist anymore (probably because
// an onchange triggered when unselecting the previous one // an onchange triggered when unselecting the previous one
// removes rows) // removes rows)
return $.Deferred().reject(); return Promise.reject();
} }
// Notify the controller we want to make a record editable // Notify the controller we want to make a record editable
var def = $.Deferred(); return new Promise(function (resolve) {
self.trigger_up('edit_line', { self.trigger_up('edit_line', {
index: rowIndex, index: rowIndex,
onSuccess: def.resolve.bind(def), onSuccess: resolve,
});
}); });
return def;
}); });
}, },
@@ -778,7 +787,7 @@ ListRenderer.include({
// but we do want to unselect current row // but we do want to unselect current row
var self = this; var self = this;
this.unselectRow().then(function () { 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 * @param {OdooEvent} ev
*/ */
_onNavigationMove: function (ev) { _onNavigationMove: function (ev) {
var self = this;
ev.stopPropagation(); // stop the event, the action is done by this renderer ev.stopPropagation(); // stop the event, the action is done by this renderer
switch (ev.data.direction) { switch (ev.data.direction) {
case 'previous': case 'previous':
if (this.currentFieldIndex > 0) { if (this.currentFieldIndex > 0) {
this._selectCell(this.currentRow, this.currentFieldIndex - 1, {wrap: false}) this._selectCell(this.currentRow, this.currentFieldIndex - 1, {wrap: false})
.fail(this._moveToPreviousLine.bind(this)); .guardedCatch(this._moveToPreviousLine.bind(this));
} else { } else {
this._moveToPreviousLine(); this._moveToPreviousLine();
} }
@@ -882,7 +892,7 @@ ListRenderer.include({
if (column.attrs.name === lastWidget.name) { if (column.attrs.name === lastWidget.name) {
if (this.currentRow + 1 < this.state.data.length) { if (this.currentRow + 1 < this.state.data.length) {
this._selectCell(this.currentRow+1, 0, {wrap:false}) this._selectCell(this.currentRow+1, 0, {wrap:false})
.fail(this._moveToNextLine.bind(this)); .guardedCatch(this._moveToNextLine.bind(this));
} else { } else {
var currentRowData = this.state.data[this.currentRow]; var currentRowData = this.state.data[this.currentRow];
if (currentRowData.isDirty(currentRowData.id)) { if (currentRowData.isDirty(currentRowData.id)) {
@@ -895,7 +905,7 @@ ListRenderer.include({
} else { } else {
if (this.currentFieldIndex + 1 < this.columns.length) { if (this.currentFieldIndex + 1 < this.columns.length) {
this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false}) this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
.fail(this._moveToNextLine.bind(this)); .guardedCatch(this._moveToNextLine.bind(this));
} else { } else {
this._moveToNextLine(); this._moveToNextLine();
} }
@@ -911,8 +921,10 @@ ListRenderer.include({
ev.data.originalEvent.stopPropagation(); ev.data.originalEvent.stopPropagation();
this.trigger_up('discard_changes', { this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID, 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; break;
} }
}, },
@@ -250,9 +250,10 @@ var ListRenderer = BasicRenderer.extend({
* @returns {jQueryElement} a jquery element <tbody> * @returns {jQueryElement} a jquery element <tbody>
*/ */
_renderBody: function () { _renderBody: function () {
var self = this;
var $rows = this._renderRows(); var $rows = this._renderRows();
while ($rows.length < 4) { while ($rows.length < 4) {
$rows.push(this._renderEmptyRow()); $rows.push(self._renderEmptyRow());
} }
return $('<tbody>').append($rows); return $('<tbody>').append($rows);
}, },
@@ -306,7 +307,6 @@ var ListRenderer = BasicRenderer.extend({
} }
if (node.attrs.widget || (options && options.renderWidgets)) { if (node.attrs.widget || (options && options.renderWidgets)) {
var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode')); var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
this._handleAttributes($el, node);
return $td.append($el); return $td.append($el);
} }
var name = node.attrs.name; var name = node.attrs.name;
@@ -395,7 +395,7 @@ var ListRenderer = BasicRenderer.extend({
* *
* @private * @private
* @param {Object} group * @param {Object} group
* @returns {JQueryElement} the pager's $el * @returns {jQueryElement} the pager's $el
*/ */
_renderGroupPager: function (group) { _renderGroupPager: function (group) {
var pager = new Pager(this, group.count, group.offset + 1, group.limit); 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 // register the pager so that it can be destroyed on next rendering
this.pagers.push(pager); this.pagers.push(pager);
var fragment = document.createDocumentFragment(); var pagerProm = pager._widgetRenderAndInsert(function () {}); // start the pager
pager.appendTo(fragment); // starts the pager this.defs.push(pagerProm);
return pager.$el; var $el = $('<div>');
pagerProm.then(function () {
$el.replaceWith(pager.$el);
});
return $el;
}, },
/** /**
* Render the row that represent a group * Render the row that represent a group
@@ -514,10 +519,9 @@ var ListRenderer = BasicRenderer.extend({
* with the name of each fields * with the name of each fields
* *
* @private * @private
* @param {boolean} isGrouped
* @returns {jQueryElement} a <thead> element * @returns {jQueryElement} a <thead> element
*/ */
_renderHeader: function (isGrouped) { _renderHeader: function () {
var $tr = $('<tr>') var $tr = $('<tr>')
.append(_.map(this.columns, this._renderHeaderCell.bind(this))); .append(_.map(this.columns, this._renderHeaderCell.bind(this)));
if (this.hasSelectors) { if (this.hasSelectors) {
@@ -585,11 +589,9 @@ var ListRenderer = BasicRenderer.extend({
*/ */
_renderRow: function (record) { _renderRow: function (record) {
var self = this; var self = this;
this.defs = []; // TODO maybe wait for those somewhere ? var $cells = this.columns.map(function (node, index) {
var $cells = _.map(this.columns, function (node, index) {
return self._renderBodyCell(record, node, index, { mode: 'readonly' }); return self._renderBodyCell(record, node, index, { mode: 'readonly' });
}); });
delete this.defs;
var $tr = $('<tr/>', { class: 'o_data_row' }) var $tr = $('<tr/>', { class: 'o_data_row' })
.data('id', record.id) .data('id', record.id)
@@ -608,7 +610,7 @@ var ListRenderer = BasicRenderer.extend({
* @returns {jQueryElement[]} a list of <tr> * @returns {jQueryElement[]} a list of <tr>
*/ */
_renderRows: function () { _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 * A 'selector' is the small checkbox on the left of a record in a list
@@ -638,52 +640,60 @@ var ListRenderer = BasicRenderer.extend({
* *
* @override * @override
* @private * @private
* returns {Deferred} this deferred is resolved immediately * @returns {Promise} resolved when the view has been rendered
*/ */
_renderView: function () { _renderView: function () {
var self = this; var self = this;
this.$el var oldPagers = this.pagers;
.removeClass('table-responsive')
.empty();
// destroy the previously instantiated pagers, if any
_.invoke(this.pagers, 'destroy');
this.pagers = []; this.pagers = [];
var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
// display the no content helper if there is no data to display // display the no content helper if there is no data to display
var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
if (displayNoContentHelper) { if (displayNoContentHelper) {
// destroy the previously instantiated pagers, if any
_.invoke(oldPagers, 'destroy');
this.$el.removeClass('table-responsive');
this.$el.html(this._renderNoContentHelper()); 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'); var orderedBy = this.state.orderedBy;
this.$el.addClass('table-responsive') this.hasHandle = orderedBy.length === 0 || orderedBy[0].name === this.handleField;
.append($table);
this._computeAggregates(); 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_grouped', this.isGrouped);
$table.toggleClass('o_list_view_ungrouped', !this.isGrouped); $table.toggleClass('o_list_view_ungrouped', !this.isGrouped);
this.hasHandle = this.state.orderedBy.length === 0 || var defs = [];
this.state.orderedBy[0].name === this.handleField; this.defs = defs;
if (this.isGrouped) { if (this.isGrouped) {
$table $table.append(this._renderHeader());
.append(this._renderHeader(true)) $table.append(this._renderGroups(this.state.data));
.append(this._renderGroups(this.state.data)) $table.append(this._renderFooter());
.append(this._renderFooter());
} else { } else {
$table $table.append(this._renderHeader());
.append(this._renderHeader()) $table.append(this._renderBody());
.append(this._renderBody()) $table.append(this._renderFooter());
.append(this._renderFooter());
} }
if (this.selection.length) { delete this.defs;
var $checked_rows = this.$('tr').filter(function (index, el) {
return _.contains(self.selection, $(el).data('id')); var prom = Promise.all(defs).then(function () {
}); // destroy the previously instantiated pagers, if any
$checked_rows.find('.o_list_record_selector input').prop('checked', true); _.invoke(oldPagers, 'destroy');
}
return this._super(); 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 * Each line can be decorated according to a few simple rules. The arch
@@ -45,10 +45,10 @@ var PivotModel = AbstractModel.extend({
/** /**
* Close a header. This method is actually synchronous, but returns a * Close a header. This method is actually synchronous, but returns a
* deferred. * promise.
* *
* @param {any} headerID * @param {any} headerID
* @returns {Deferred} * @returns {Promise}
*/ */
closeHeader: function (headerID) { closeHeader: function (headerID) {
var header = this.data.headers[headerID]; var header = this.data.headers[headerID];
@@ -58,7 +58,7 @@ var PivotModel = AbstractModel.extend({
header.root.groupbys.splice(newGroupbyLength); header.root.groupbys.splice(newGroupbyLength);
}, },
/** /**
* @returns {Deferred} * @returns {Promise}
*/ */
expandAll: function () { expandAll: function () {
return this._loadData(); return this._loadData();
@@ -118,8 +118,8 @@ var PivotModel = AbstractModel.extend({
}); });
})); }));
} }
return $.when.apply(null, defs).then(function () { return Promise.all(defs).then(function () {
var results = Array.prototype.slice.call(arguments); var results = Array.prototype.slice.call(arguments[0]);
var data = []; var data = [];
var comparisonData = []; var comparisonData = [];
_.each(results, function (result) { _.each(results, function (result) {
@@ -358,7 +358,7 @@ var PivotModel = AbstractModel.extend({
* @param {string[]} params.compare * @param {string[]} params.compare
* @param {Object} params.fields * @param {Object} params.fields
* @param {string} params.default_order * @param {string} params.default_order
* @returns {Deferred} * @returns {Promise}
*/ */
load: function (params) { load: function (params) {
var self = this; var self = this;
@@ -394,7 +394,7 @@ var PivotModel = AbstractModel.extend({
* @override * @override
* @param {any} handle this parameter is ignored * @param {any} handle this parameter is ignored
* @param {Object} params * @param {Object} params
* @returns {Deferred} * @returns {Promise}
*/ */
reload: function (handle, params) { reload: function (handle, params) {
var self = this; var self = this;
@@ -507,14 +507,14 @@ var PivotModel = AbstractModel.extend({
* Toggle the active state for a given measure, then reload the data. * Toggle the active state for a given measure, then reload the data.
* *
* @param {string} field * @param {string} field
* @returns {Deferred} * @returns {Promise}
*/ */
toggleMeasure: function (field) { toggleMeasure: function (field) {
if (_.contains(this.data.measures, field)) { if (_.contains(this.data.measures, field)) {
this.data.measures = _.without(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 // in this case, we already have all data in memory, no need to
// actually reload a lesser amount of information // actually reload a lesser amount of information
return $.when(); return Promise.resolve();
} else { } else {
this.data.measures.push(field); this.data.measures.push(field);
} }
@@ -705,7 +705,7 @@ var PivotModel = AbstractModel.extend({
return result; return result;
}, },
/** /**
* @returns {Deferred} * @returns {Promise}
*/ */
_loadData: function () { _loadData: function () {
var self = this; var self = this;
@@ -755,8 +755,7 @@ var PivotModel = AbstractModel.extend({
})); }));
} }
return this._loadDataDropPrevious.add($.when.apply(null, defs)).then(function () { return this._loadDataDropPrevious.add(Promise.all(defs)).then(function (results) {
var results = Array.prototype.slice.call(arguments);
var data = []; var data = [];
var comparisonData = []; var comparisonData = [];
_.each(results, function (result) { _.each(results, function (result) {
@@ -72,7 +72,7 @@ var PivotRenderer = AbstractRenderer.extend({
/** /**
* @override * @override
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_render: function () { _render: function () {
if (!this._hasContent()) { if (!this._hasContent()) {
@@ -38,7 +38,7 @@ var StandaloneFieldManagerMixin = _.extend({}, FieldManagerMixin, {
* @param {string} id basicModel Id for the changed record * @param {string} id basicModel Id for the changed record
* @param {string[]} fields the fields (names) that have been changed * @param {string[]} fields the fields (names) that have been changed
* @param {OdooEvent} event the event that triggered the change * @param {OdooEvent} event the event that triggered the change
* @returns {Deferred} * @returns {Promise}
*/ */
_confirmChange: function (id, fields, event) { _confirmChange: function (id, fields, event) {
var result = FieldManagerMixin._confirmChange.apply(this, arguments); var result = FieldManagerMixin._confirmChange.apply(this, arguments);
+9 -10
View File
@@ -128,7 +128,7 @@ var FormViewDialog = ViewDialog.extend({
text: (multi_select ? _t("Save & Close") : _t("Save")), text: (multi_select ? _t("Save & Close") : _t("Save")),
classes: "btn-primary", classes: "btn-primary",
click: function () { 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"), text: _t("Save & New"),
classes: "btn-primary", classes: "btn-primary",
click: function () { click: function () {
this._save() self._save()
.then(self.form_view.createRecord.bind(self.form_view, self.parentID)) .then(self.form_view.createRecord.bind(self.form_view, self.parentID))
.then(function () { .then(function () {
if (!self.deletable) { if (!self.deletable) {
@@ -183,7 +183,7 @@ var FormViewDialog = ViewDialog.extend({
var FormView = view_registry.get('form'); var FormView = view_registry.get('form');
var fields_view_def; var fields_view_def;
if (this.options.fields_view) { if (this.options.fields_view) {
fields_view_def = $.when(this.options.fields_view); fields_view_def = Promise.resolve(this.options.fields_view);
} else { } else {
fields_view_def = this.loadFieldView(this.res_model, this.context, this.options.view_id, 'form'); 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) { if (self.recordID && self.shouldSaveLocally) {
self.model.save(self.recordID, {savePoint: true}); self.model.save(self.recordID, {savePoint: true});
} }
self.form_view.appendTo(fragment) return self.form_view.appendTo(fragment)
.then(function () { .then(function () {
self.opened().always(function () { self.opened().then(function () {
var $buttons = $('<div>'); var $buttons = $('<div>');
self.form_view.renderButtons($buttons); self.form_view.renderButtons($buttons);
if ($buttons.children().length) { if ($buttons.children().length) {
@@ -226,7 +226,7 @@ var FormViewDialog = ViewDialog.extend({
in_DOM: true, in_DOM: true,
}); });
}); });
_super(); return _super();
}); });
}); });
@@ -255,7 +255,7 @@ var FormViewDialog = ViewDialog.extend({
/** /**
* @private * @private
* @returns {Deferred} * @returns {Promise}
*/ */
_save: function () { _save: function () {
var self = this; var self = this;
@@ -332,7 +332,7 @@ var SelectCreateDialog = ViewDialog.extend({
} }
var self = this; var self = this;
var _super = this._super.bind(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(this.setup.bind(this))
.then(function (fragment) { .then(function (fragment) {
self.opened().then(function () { self.opened().then(function () {
@@ -342,9 +342,8 @@ var SelectCreateDialog = ViewDialog.extend({
}); });
self.set_buttons(self.__buttons); self.set_buttons(self.__buttons);
}); });
_super(); return _super();
}); });
return this;
}, },
setup: function (fieldsViews) { setup: function (fieldsViews) {
+1 -1
View File
@@ -63,7 +63,7 @@ var viewUtils = {
*/ */
renderButtonFromNode: function (node, options) { renderButtonFromNode: function (node, options) {
var btnOptions = { 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, icon: node.attrs.icon,
}; };
if (options && options.extraClass) { if (options && options.extraClass) {
@@ -108,26 +108,27 @@ var AttachDocument = Widget.extend({
* @private * @private
*/ */
_onFileLoaded: function () { _onFileLoaded: function () {
var self = this, var self = this;
def = $.Deferred(); return new Promise(function (resolve) {
if (self.node.attrs.action) {
if (self.node.attrs.action) { // the first argument isn't a file but the jQuery.Event
// the first argument isn't a file but the jQuery.Event var files = Array.prototype.slice.call(arguments, 1);
var files = Array.prototype.slice.call(arguments, 1); self._rpc({
self._rpc({ model: self.res_model,
model: self.res_model, method: self.node.attrs.action,
method: self.node.attrs.action, args: [self.res_id],
args: [self.res_id], kwargs: {
kwargs: { attachment_ids: _.map(files, function (file) {
'attachment_ids': _.map(files, function (file) {return file.id;}), return file.id;
} }),
}).then( function () { }
def.resolve(); }).then(function () {
}); resolve();
} else { });
def.resolve(); } else {
} resolve();
return $.when(def).then(function () { }
}).then(function () {
self.trigger_up('reload'); self.trigger_up('reload');
framework.unblockUI(); framework.unblockUI();
}); });
@@ -20,7 +20,7 @@ var ChangePassword = AbstractAction.extend({
* @fixme: weird interaction with the parent for the $buttons handling * @fixme: weird interaction with the parent for the $buttons handling
* *
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
var self = this; var self = this;
@@ -37,7 +37,7 @@ var ChangePassword = AbstractAction.extend({
fields: $('form[name=change_password_form]').serializeArray() fields: $('form[name=change_password_form]').serializeArray()
} }
}) })
.done(function (result) { .then(function (result) {
if (result.error) { if (result.error) {
self._display_error(result); self._display_error(result);
} else { } else {
+19 -19
View File
@@ -56,7 +56,7 @@ var DataExport = Dialog.extend({
*/ */
start: function () { start: function () {
var self = this; 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%;" // The default for the ".modal_content" element is "max-height: 100%;"
// but we want it to always expand to "height: 100%;" for this modal. // 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.$fieldsList = this.$('.o_fields_list');
this.$importCompatRadios = this.$('.o_import_compat input'); this.$importCompatRadios = this.$('.o_import_compat input');
defs.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats)); proms.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats));
defs.push(this._onChangeCompatibleInput()); proms.push(this._onChangeCompatibleInput());
defs.push(this.getParent().getActiveDomain().then(function (domain) { proms.push(this.getParent().getActiveDomain().then(function (domain) {
if (domain === undefined) { if (domain === undefined) {
self.idsToExport = self.getParent().getSelectedIds(); self.idsToExport = self.getParent().getSelectedIds();
self.domain = self.record.domain; 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) { _.each(self.records, function (record, key) {
if (_.contains(self.defaultExportFields, key)) { if (_.contains(self.defaultExportFields, key)) {
self._addField(key, record.string); 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'); 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 $radio = $('<input/>', {type: 'radio', value: format.tag, name: 'o_export_format_name'});
var $label = $('<span/>', {html: format.label}); var $label = $('<span/>', {html: format.label});
@@ -192,16 +192,16 @@ var DataExport = Dialog.extend({
*/ */
_onExpandAction: function (record) { _onExpandAction: function (record) {
var self = this; var self = this;
if (!record['children']) { if (!record.children) {
return; return;
} }
var model = record['params']['model']; var model = record.params.model;
var prefix = record['params']['prefix']; var prefix = record.params.prefix;
var name = record['params']['name']; var name = record.params.name;
var excludeFields = []; var excludeFields = [];
if (record['relation_field']) { if (record.relation_field) {
excludeFields.push(record['relation_field']); excludeFields.push(record.relation_field);
} }
if (!record.loaded) { if (!record.loaded) {
@@ -212,11 +212,11 @@ var DataExport = Dialog.extend({
prefix: prefix, prefix: prefix,
parent_name: name, parent_name: name,
import_compat: !!this.$importCompatRadios.filter(':checked').val(), import_compat: !!this.$importCompatRadios.filter(':checked').val(),
parent_field_type: record['field_type'], parent_field_type: record.field_type,
parent_field: record['params']['parent_field'], parent_field: record.params.parent_field,
exclude: excludeFields, exclude: excludeFields,
}, },
}).done(function (results) { }).then(function (results) {
record.loaded = true; record.loaded = true;
self._onShowData(results, record.id); self._onShowData(results, record.id);
}); });
@@ -294,7 +294,7 @@ var DataExport = Dialog.extend({
var self = this; var self = this;
if (this.$('.o_exported_lists_select').is(':hidden')) { if (this.$('.o_exported_lists_select').is(':hidden')) {
this.$('.o_exported_lists').show(); this.$('.o_exported_lists').show();
return $.when(); return Promise.resolve();
} }
return this._rpc({ return this._rpc({
@@ -355,7 +355,7 @@ var DataExport = Dialog.extend({
model: this.record.model, model: this.record.model,
import_compat: this.isCompatibleMode, import_compat: this.isCompatibleMode,
}, },
}).done(function (records) { }).then(function (records) {
var compatibleFields = _.map(records, function (record) { return record.id; }); var compatibleFields = _.map(records, function (record) { return record.id; });
self.$fieldsList self.$fieldsList
.find('option') .find('option')
@@ -188,11 +188,14 @@ var DomainTree = DomainNode.extend({
}, },
/** /**
* @see DomainNode.start * @see DomainNode.start
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this._postRender(); 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 * @private
* @param {jQuery} $to - the jQuery node to which the children must be added * @param {jQuery} $to - the jQuery node to which the children must be added
* @returns {Deferred} * @returns {Promise}
*/ */
_renderChildrenTo: function ($to) { _renderChildrenTo: function ($to) {
var $div = $("<div/>"); var $div = $("<div/>");
return $.when.apply($, _.map(this.children, (function (child) { return Promise.all(_.map(this.children, (function (child) {
return child.appendTo($div); return child.appendTo($div);
}).bind(this))).then((function () { }).bind(this))).then((function () {
_.each(this.children, function (child) { _.each(this.children, function (child) {
@@ -485,11 +488,11 @@ var DomainSelector = DomainTree.extend({
* does nothing. * does nothing.
* *
* @param {string} domain * @param {string} domain
* @returns {Deferred} resolved when the rerendering is finished * @returns {Promise} resolved when the rerendering is finished
*/ */
setDomain: function (domain) { setDomain: function (domain) {
if (domain === Domain.prototype.arrayToString(this.getDomain())) { if (domain === Domain.prototype.arrayToString(this.getDomain())) {
return $.when(); return Promise.resolve();
} }
var parsedDomain = this._parseDomain(domain); var parsedDomain = this._parseDomain(domain);
if (parsedDomain) { if (parsedDomain) {
@@ -542,7 +545,7 @@ var DomainSelector = DomainTree.extend({
* This method is ugly but achieves the right behavior without flickering. * This method is ugly but achieves the right behavior without flickering.
* *
* @param {Array|string} domain * @param {Array|string} domain
* @returns {Deferred} * @returns {Promise}
*/ */
_redraw: function (domain) { _redraw: function (domain) {
var oldChildren = this.children.slice(); var oldChildren = this.children.slice();
@@ -637,7 +640,7 @@ var DomainLeaf = DomainNode.extend({
* Prepares the information the rendering of the widget will need by * Prepares the information the rendering of the widget will need by
* pre-instantiating its internal field selector widget. * pre-instantiating its internal field selector widget.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
willStart: function () { willStart: function () {
var defs = [this._super.apply(this, arguments)]; var defs = [this._super.apply(this, arguments)];
@@ -702,17 +705,17 @@ var DomainLeaf = DomainNode.extend({
}).bind(this))); }).bind(this)));
} }
return $.when.apply($, wDefs); return Promise.all(wDefs);
} }
}).bind(this))); }).bind(this)));
return $.when.apply($, defs); return Promise.all(defs);
}, },
/** /**
* @see DomainNode.start * @see DomainNode.start
* Appends the prepared field selector and value widget. * Appends the prepared field selector and value widget.
* *
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_info, > .o_domain_leaf_edition")); // place the field selector this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_info, > .o_domain_leaf_edition")); // place the field selector
@@ -45,10 +45,10 @@ return Dialog.extend({
// this restores default modal height (bootstrap) and allows field selector to overflow // this restores default modal height (bootstrap) and allows field selector to overflow
self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto'); self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto');
}); });
return $.when( return Promise.all([
this._super.apply(this, arguments), this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el) this.domainSelector.appendTo(this.$el)
); ]);
}, },
}); });
}); });
@@ -29,7 +29,7 @@ var IFrameWidget = Widget.extend({
}, },
/** /**
* @override * @override
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this.$el.css({height: '100%', width: '100%', border: 0}); this.$el.css({height: '100%', width: '100%', border: 0});
@@ -112,17 +112,17 @@ var ModelFieldSelector = Widget.extend({
}, },
/** /**
* @see Widget.willStart() * @see Widget.willStart()
* @returns {Deferred} * @returns {Promise}
*/ */
willStart: function () { willStart: function () {
return $.when( return Promise.all([
this._super.apply(this, arguments), this._super.apply(this, arguments),
this._prefill() this._prefill()
); ]);
}, },
/** /**
* @see Widget.start * @see Widget.start
* @returns {Deferred} * @returns {Promise}
*/ */
start: function () { start: function () {
this.$value = this.$(".o_field_selector_value"); this.$value = this.$(".o_field_selector_value");
@@ -162,11 +162,11 @@ var ModelFieldSelector = Widget.extend({
* Saves a new field chain (array) and re-render. * Saves a new field chain (array) and re-render.
* *
* @param {string[]} chain - the new field chain * @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) { setChain: function (chain) {
if (_.isEqual(chain, this.chain)) { if (_.isEqual(chain, this.chain)) {
return $.when(); return Promise.resolve();
} }
this.chain = chain; this.chain = chain;
@@ -290,14 +290,14 @@ var ModelFieldSelector = Widget.extend({
* chain. * chain.
* *
* @private * @private
* @returns {Deferred} resolved once the whole field chain has been * @returns {Promise} resolved once the whole field chain has been
* processed * processed
*/ */
_prefill: function () { _prefill: function () {
this.pages = []; this.pages = [];
return this._pushPageData(this.model).then((function () { return this._pushPageData(this.model).then((function () {
this._validate(true); 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)); }).bind(this));
function processChain(chain) { 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 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)); return this._pushPageData(field.relation).then(processChain.bind(this, chain));
} else if (field && chain.length === 0) { // Last node fetched } else if (field && chain.length === 0) { // Last node fetched
return $.when(); return Promise.resolve();
} else { // Wrong node chain } else { // Wrong node chain
this._validate(false); this._validate(false);
} }
return $.when(); return Promise.resolve();
} }
}, },
/** /**
@@ -318,12 +318,12 @@ var ModelFieldSelector = Widget.extend({
* *
* @private * @private
* @param {string} model - the model name whose fields have to be fetched * @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) { _pushPageData: function (model) {
var def; var def;
if (this.model === model && this.options.fields) { 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 { } else {
def = this._getModelFieldsFromCache(model, this.options.filters); def = this._getModelFieldsFromCache(model, this.options.filters);
} }
@@ -80,14 +80,12 @@ var NameAndSignature = Widget.extend({
*/ */
willStart: function () { willStart: function () {
var self = this; var self = this;
return Promise.all([
return $.when(
this._super.apply(this, arguments), this._super.apply(this, arguments),
this._rpc({route: '/web/sign/get_fonts/' + self.defaultFont}).then(function (data) { this._rpc({route: '/web/sign/get_fonts/' + self.defaultFont}).then(function (data) {
self.fonts = data; self.fonts = data;
}) })
); ]);
}, },
/** /**
* Finds the DOM elements, initializes the signature area, * Finds the DOM elements, initializes the signature area,
@@ -217,7 +215,7 @@ var NameAndSignature = Widget.extend({
resetSignature: function () { resetSignature: function () {
if (!this.$signatureField) { if (!this.$signatureField) {
// no action if called before start // no action if called before start
return $.Deferred().reject(); return Promise.reject();
} }
// recompute size based on the current width // recompute size based on the current width
this.$signatureField.css({width: 'unset'}); this.$signatureField.css({width: 'unset'});
@@ -248,7 +246,7 @@ var NameAndSignature = Widget.extend({
this.focusName(); this.focusName();
return $.Deferred().resolve(); return Promise.resolve();
}, },
/** /**
* Changes the signature mode. Toggles the display of the relevant * Changes the signature mode. Toggles the display of the relevant
@@ -67,7 +67,7 @@ var PieChart = Widget.extend({
self.controller = controller; self.controller = controller;
return self.controller.appendTo(document.createDocumentFragment()); return self.controller.appendTo(document.createDocumentFragment());
}); });
return $.when(def1, def2); return Promise.all([def1, def2]);
}, },
/** /**
* @override * @override
@@ -28,12 +28,6 @@ var SwitchCompanyMenu = Widget.extend({
this.isMobile = config.device.isMobile; this.isMobile = config.device.isMobile;
this._onClick = _.debounce(this._onClick, 1500, true); this._onClick = _.debounce(this._onClick, 1500, true);
}, },
/**
* @override
*/
willStart: function () {
return session.user_companies ? this._super() : $.Deferred().reject();
},
/** /**
* @override * @override
*/ */
@@ -81,9 +75,9 @@ var SwitchCompanyMenu = Widget.extend({
}); });
}, },
}); });
if (session.user_companies) {
SystrayMenu.Items.push(SwitchCompanyMenu); SystrayMenu.Items.push(SwitchCompanyMenu);
}
return SwitchCompanyMenu; return SwitchCompanyMenu;
}); });
@@ -53,23 +53,23 @@ QUnit.module('ActionManager', {
}; };
}, },
}, function () { }, function () {
QUnit.test('uses a mobile-friendly view by default (if possible)', function (assert) { QUnit.test('uses a mobile-friendly view by default (if possible)', async function (assert) {
assert.expect(4); assert.expect(4);
var actionManager = createActionManager({ var actionManager = await createActionManager({
actions: this.actions, actions: this.actions,
archs: this.archs, archs: this.archs,
data: this.data, data: this.data,
}); });
// should default on a mobile-friendly view (kanban) for action 1 // should default on a mobile-friendly view (kanban) for action 1
actionManager.doAction(1); await actionManager.doAction(1);
assert.containsNone(actionManager, '.o_list_view'); assert.containsNone(actionManager, '.o_list_view');
assert.containsOnce(actionManager, '.o_kanban_view'); assert.containsOnce(actionManager, '.o_kanban_view');
// there is no mobile-friendly view for action 2, should use the first one (list) // there is no mobile-friendly view for action 2, should use the first one (list)
actionManager.doAction(2); await actionManager.doAction(2);
assert.containsOnce(actionManager, '.o_list_view'); assert.containsOnce(actionManager, '.o_list_view');
assert.containsNone(actionManager, '.o_kanban_view'); assert.containsNone(actionManager, '.o_kanban_view');
@@ -77,10 +77,10 @@ QUnit.module('ActionManager', {
actionManager.destroy(); actionManager.destroy();
}); });
QUnit.test('lazy load mobile-friendly view', function (assert) { QUnit.test('lazy load mobile-friendly view', async function (assert) {
assert.expect(11); assert.expect(11);
var actionManager = createActionManager({ var actionManager = await createActionManager({
actions: this.actions, actions: this.actions,
archs: this.archs, archs: this.archs,
data: this.data, data: this.data,
@@ -89,7 +89,7 @@ QUnit.module('ActionManager', {
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
}); });
actionManager.loadState({ await actionManager.loadState({
action: 1, action: 1,
view_type: 'form', view_type: 'form',
}); });
@@ -99,7 +99,7 @@ QUnit.module('ActionManager', {
assert.containsOnce(actionManager, '.o_form_view'); assert.containsOnce(actionManager, '.o_form_view');
// go back to lazy loaded view // go back to lazy loaded view
testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb .breadcrumb-item:first')); await testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb .breadcrumb-item:first'));
assert.containsNone(actionManager, '.o_form_view'); assert.containsNone(actionManager, '.o_form_view');
assert.containsNone(actionManager, '.o_list_view'); assert.containsNone(actionManager, '.o_list_view');
assert.containsOnce(actionManager, '.o_kanban_view'); assert.containsOnce(actionManager, '.o_kanban_view');
@@ -114,16 +114,16 @@ QUnit.module('ActionManager', {
actionManager.destroy(); actionManager.destroy();
}); });
QUnit.test('view switcher button should be displayed in dropdown on mobile screens', function (assert) { QUnit.test('view switcher button should be displayed in dropdown on mobile screens', async function (assert) {
assert.expect(3); assert.expect(3);
var actionManager = createActionManager({ var actionManager = await createActionManager({
actions: this.actions, actions: this.actions,
archs: this.archs, archs: this.archs,
data: this.data, data: this.data,
}); });
actionManager.doAction(1); await actionManager.doAction(1);
assert.containsOnce(actionManager.$('.o_control_panel'), '.o_cp_switch_buttons button[data-toggle="dropdown"]'); assert.containsOnce(actionManager.$('.o_control_panel'), '.o_cp_switch_buttons button[data-toggle="dropdown"]');
assert.hasClass(actionManager.$('.o_cp_switch_buttons .o_cp_switch_kanban'), 'active'); assert.hasClass(actionManager.$('.o_cp_switch_buttons .o_cp_switch_kanban'), 'active');
File diff suppressed because it is too large Load Diff
@@ -6,10 +6,14 @@ var testUtils = require('web.test_utils');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
QUnit.module('KeyboardNavigationMixin', function () { QUnit.module('KeyboardNavigationMixin', function () {
QUnit.test('aria-keyshortcuts is added on elements with accesskey', function (assert) { QUnit.test('aria-keyshortcuts is added on elements with accesskey', async function (assert) {
assert.expect(1); assert.expect(1);
var $target = $('#qunit-fixture'); var $target = $('#qunit-fixture');
var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, { var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, {
init: function () {
this._super.apply(this, arguments);
KeyboardNavigationMixin.init.call(this);
},
start: function () { start: function () {
var $button = $('<button>').text('Click Me!').attr('accesskey', 'o'); var $button = $('<button>').text('Click Me!').attr('accesskey', 'o');
// we need to define the accesskey because it will not be assigned on invisible buttons // we need to define the accesskey because it will not be assigned on invisible buttons
@@ -19,7 +23,7 @@ QUnit.module('KeyboardNavigationMixin', function () {
}); });
var parent = testUtils.createParent({}); var parent = testUtils.createParent({});
var w = new KeyboardWidget(parent); var w = new KeyboardWidget(parent);
w.appendTo($target); await w.appendTo($target);
// minimum set of attribute to generate a native event that works with the mixin // minimum set of attribute to generate a native event that works with the mixin
var e = new Event("keydown"); var e = new Event("keydown");
@@ -32,7 +36,7 @@ QUnit.module('KeyboardNavigationMixin', function () {
parent.destroy(); parent.destroy();
}); });
QUnit.test('keep CSS position absolute for parent of overlay', function (assert) { QUnit.test('keep CSS position absolute for parent of overlay', async function (assert) {
// If we change the CSS position of an 'absolute' element to 'relative', // If we change the CSS position of an 'absolute' element to 'relative',
// we may likely change its position on the document. Since the overlay // we may likely change its position on the document. Since the overlay
// CSS position is 'absolute', it will match the size and cover the // CSS position is 'absolute', it will match the size and cover the
@@ -42,6 +46,10 @@ QUnit.module('KeyboardNavigationMixin', function () {
var $target = $('#qunit-fixture'); var $target = $('#qunit-fixture');
var $button; var $button;
var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, { var KeyboardWidget = Widget.extend(KeyboardNavigationMixin, {
init: function () {
this._super.apply(this, arguments);
KeyboardNavigationMixin.init.call(this);
},
start: function () { start: function () {
$button = $('<button>').text('Click Me!').attr('accesskey', 'o'); $button = $('<button>').text('Click Me!').attr('accesskey', 'o');
// we need to define the accesskey because it will not be assigned on invisible buttons // we need to define the accesskey because it will not be assigned on invisible buttons
@@ -51,7 +59,7 @@ QUnit.module('KeyboardNavigationMixin', function () {
}); });
var parent = testUtils.createParent({}); var parent = testUtils.createParent({});
var w = new KeyboardWidget(parent); var w = new KeyboardWidget(parent);
w.appendTo($target); await w.appendTo($target);
$button.css('position', 'absolute'); $button.css('position', 'absolute');
+16 -18
View File
@@ -9,40 +9,41 @@ QUnit.module('chrome', {}, function () {
QUnit.module('Pager'); QUnit.module('Pager');
QUnit.test('basic stuff', function (assert) { QUnit.test('basic stuff', async function (assert) {
assert.expect(2); assert.expect(2);
var pager = new Pager(null, 10, 1, 4); var pager = new Pager(null, 10, 1, 4);
pager.appendTo($('#qunit-fixture')); pager.appendTo($('#qunit-fixture'));
await testUtils.nextTick();
assert.strictEqual(pager.state.current_min, 1, assert.strictEqual(pager.state.current_min, 1,
"current_min should be set to 1"); "current_min should be set to 1");
// click on next // click on next
testUtils.dom.click(pager.$('.o_pager_next')); await testUtils.dom.click(pager.$('.o_pager_next'));
assert.strictEqual(pager.state.current_min, 5, assert.strictEqual(pager.state.current_min, 5,
"current_min should now be 5"); "current_min should now be 5");
pager.destroy(); pager.destroy();
}); });
QUnit.test('edit the pager', function (assert) { QUnit.test('edit the pager', async function (assert) {
assert.expect(5); assert.expect(5);
var pager = new Pager(null, 10, 1, 4); var pager = new Pager(null, 10, 1, 4);
pager.appendTo($('#qunit-fixture')); pager.appendTo($('#qunit-fixture'));
await testUtils.nextTick();
// enter edition // enter edition
testUtils.dom.click(pager.$('.o_pager_value')); await testUtils.dom.click(pager.$('.o_pager_value'));
assert.containsOnce(pager, 'input', assert.containsOnce(pager, 'input',
"the pager should contain an input"); "the pager should contain an input");
assert.strictEqual(pager.$('input').val(), '1-4', assert.strictEqual(pager.$('input').val(), '1-4',
"the input should have correct value"); "the input should have correct value");
// change the limit // change the limit
pager.$('input') await testUtils.fields.editInput(pager.$('input'), '1-6');
.val('1-6') await testUtils.fields.triggerKeydown(pager.$('input'), 'enter');
.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); // trigger the change
assert.strictEqual(pager.state.limit, 6, assert.strictEqual(pager.state.limit, 6,
"the limit should have been updated"); "the limit should have been updated");
assert.strictEqual(pager.state.current_min, 1, assert.strictEqual(pager.state.current_min, 1,
@@ -53,42 +54,39 @@ QUnit.module('chrome', {}, function () {
pager.destroy(); pager.destroy();
}); });
QUnit.test('disabling the pager', function (assert) { QUnit.test('disabling the pager', async function (assert) {
var done = assert.async();
assert.expect(4); assert.expect(4);
var pager = new Pager(null, 10, 1, 4); var pager = new Pager(null, 10, 1, 4);
pager.appendTo($('#qunit-fixture')); pager.appendTo($('#qunit-fixture'));
await testUtils.nextTick();
pager.disable(); await pager.disable();
// try to go to the next or previous pages // try to go to the next or previous pages
testUtils.dom.click(pager.$('.o_pager_next')); await testUtils.dom.click(pager.$('.o_pager_next'));
assert.strictEqual(pager.state.current_min, 1, assert.strictEqual(pager.state.current_min, 1,
"current_min should still be 1"); "current_min should still be 1");
testUtils.dom.click(pager.$('.o_pager_previous')); await testUtils.dom.click(pager.$('.o_pager_previous'));
assert.strictEqual(pager.state.current_min, 1, assert.strictEqual(pager.state.current_min, 1,
"current_min should still be 1"); "current_min should still be 1");
// try to change the limit // try to change the limit
testUtils.dom.click(pager.$('.o_pager_value')); await testUtils.dom.click(pager.$('.o_pager_value'));
assert.containsNone(pager, 'input', assert.containsNone(pager, 'input',
"the pager should not contain an input"); "the pager should not contain an input");
// a common use is to disable the pager before reloading the data, and // a common use is to disable the pager before reloading the data, and
// re-enable it once they have been loaded // re-enable it once they have been loaded
// the following imulates this situation // the following emulates this situation
pager.on('pager_changed', null, function () { pager.on('pager_changed', null, function () {
pager.disable(); pager.disable();
concurrency.delay(0).then(function () { concurrency.delay(0).then(function () {
assert.ok(pager.disabled, "pager should still be disabled"); assert.ok(pager.disabled, "pager should still be disabled");
pager.destroy(); pager.destroy();
done();
}); });
}); });
pager.enable(); pager.enable();
testUtils.dom.click(pager.$('.o_pager_next')); await testUtils.dom.click(pager.$('.o_pager_next'));
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More