diff options
Diffstat (limited to 'addons/web/static/src/js/views')
42 files changed, 914 insertions, 793 deletions
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) { |
