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