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