diff --git a/addons/web/doc/guidelines.rst b/addons/web/doc/guidelines.rst index 91e1e513a05..8721d372f5b 100644 --- a/addons/web/doc/guidelines.rst +++ b/addons/web/doc/guidelines.rst @@ -123,6 +123,7 @@ you should use: * * @override method from field manager mixin * @param {string} id + * @returns {Deferred} */ _confirmSave: function (id) { diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index b1b51e69e36..17cac8e86e5 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -56,14 +56,7 @@ var TranslatableFieldMixin = { * @private */ _onTranslate: function () { - this._rpc({ - route: '/web/dataset/call_button', - params: { - model: 'ir.translation', - method: 'translate_fields', - args: [this.model, this.res_id, this.name, this.record.getContext()], - } - }).then(this.do_action.bind(this)); + this.trigger_up('translate', {fieldName: this.name, id: this.dataPointID}); }, }; 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 f1e71bcfd5a..4b38ac69403 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -19,7 +19,8 @@ var _t = core._t; var BasicController = AbstractController.extend(FieldManagerMixin, { custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { reload: '_onReload', - sidebar_data_asked: '_onSidebarDataAsked' + sidebar_data_asked: '_onSidebarDataAsked', + translate: '_onTranslate', }), /** * @override @@ -169,6 +170,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * @param {string} [recordID] - default to main recordID * @param {Object} [options] * @returns {Deferred} + * Resolved with the list of field names (whose value has been modified) + * Rejected if the record can't be saved */ saveRecord: function (recordID, options) { // Some field widgets can't detect (all) their changes immediately or @@ -367,6 +370,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * if true, the record will only be 'locally' saved: its changes * will move from the _changes key to the data key * @returns {Deferred} + * Resolved with the list of field names (whose value has been modified) + * Rejected if the record can't be saved */ _saveRecord: function (recordID, options) { recordID = recordID || this.handle; @@ -376,22 +381,24 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { savePoint: false, }); - var def = $.Deferred(); // Check if the view is in a valid state for saving // Note: it is the model's job to do nothing if there is nothing to save if (this.canBeSaved(recordID)) { - def = this.model.save(recordID, { // Save then leave edit mode + var self = this; + var def = this.model.save(recordID, { // Save then leave edit mode reload: options.reload, savePoint: options.savePoint, }); - } else { - def.reject(); // Cannot be saved, do nothing at all - } - - if (options.stayInEdit) { + if (!options.stayInEdit) { + def = def.then(function (fieldNames) { + return self._confirmSave(recordID).then(function () { + return fieldNames; + }); + }); + } return def; } else { - return def.then(this._confirmSave.bind(this, recordID)); + return $.Deferred().reject(); // Cannot be saved } }, /** @@ -498,6 +505,23 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { var sidebarEnv = this._getSidebarEnv(); event.data.callback(sidebarEnv); }, + /** + * open the translation view for the current field + * + * @private + * @param {OdooEvent} event + */ + _onTranslate: function (event) { + var record = this.model.get(event.data.id, {raw: true}); + this._rpc({ + route: '/web/dataset/call_button', + params: { + model: 'ir.translation', + method: 'translate_fields', + args: [record.model, record.res_id, event.data.fieldName, record.getContext()], + } + }).then(this.do_action.bind(this)); + }, }); return BasicController; 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 abb7242661e..ca042d63d69 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -708,6 +708,7 @@ var BasicModel = AbstractModel.extend({ * be 'locally' saved: its changes written in a _savePoint key that can * be restored later by call discardChanges with option rollback to true * @returns {Deferred} + * Resolved with the list of field names (whose value has been modified) */ save: function (record_id, options) { var self = this; @@ -723,7 +724,6 @@ var BasicModel = AbstractModel.extend({ rec._savePoint = _.extend({}, newValue); } }); - return $.when(); } var shouldReload = 'reload' in options ? options.reload : true; var method = self.isNew(record_id) ? 'create' : 'write'; @@ -742,10 +742,17 @@ var BasicModel = AbstractModel.extend({ }); } + var def = $.Deferred(); + var changedFields = Object.keys(changes); + + if (options.savePoint) { + return def.resolve(changedFields); + } + // in the case of a write, only perform the RPC if there are changes to save - if (method === 'create' || Object.keys(changes).length) { + if (method === 'create' || changedFields.length) { var args = method === 'write' ? [[record.data.id], changes] : [changes]; - return self._rpc({ + self._rpc({ model: record.model, method: method, args: args, @@ -759,23 +766,26 @@ var BasicModel = AbstractModel.extend({ record.count++; } - // Update the data directly or reload them - var def; - if (shouldReload) { - def = self._fetchRecord(record); - } else { - _.extend(record.data, record._changes); - } + var _changes = record._changes; // Erase changes as they have been applied record._changes = {}; record._isDirty = false; - return def; + // Update the data directly or reload them + if (shouldReload) { + self._fetchRecord(record).then(function (record) { + def.resolve(changedFields); + }); + } else { + _.extend(record.data, _changes); + def.resolve(changedFields); + } }); } else { - return $.when(record_id); + def.resolve(changedFields); } + return def; }); }, /** 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 31b858b1b01..99d21552be6 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -168,6 +168,35 @@ var FormController = BasicController.extend({ this._updateSidebar(); } }, + /** + * Show a warning message if the user modified a translated field. For each + * field, the notification provides a link to edit the field's translations. + * + * @override + */ + saveRecord: function () { + var result = this._super.apply(this, arguments); + if (_t.database.multi_lang) { + var self = this; + result.then(function (changedFields) { + if (!changedFields.length) { + return changedFields; + } + var fields = self.renderer.state.fields; + var alertFields = []; + for (var k = 0; k < changedFields.length; k++) { + var field = fields[changedFields[k]]; + if (field.translate) { + alertFields.push(field); + } + } + if (alertFields.length) { + self.renderer.displayTranslationAlert(alertFields); + } + }); + } + return result; + }, //-------------------------------------------------------------------------- // Private @@ -180,6 +209,7 @@ var FormController = BasicController.extend({ * @private * @override method from field manager mixin * @param {string} id + * @returns {Deferred} */ _confirmSave: function (id) { if (id === this.handle) { 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 6dc4d262022..214b880d53d 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -10,6 +10,9 @@ var qweb = core.qweb; var FormRenderer = BasicRenderer.extend({ className: "o_form_view", + events: _.extend({}, BasicRenderer.prototype.events, { + 'click .o_notification_box .oe_field_translate': '_onTranslate', + }), /** * @override */ @@ -48,6 +51,25 @@ var FormRenderer = BasicRenderer.extend({ }); return fieldNames; }, + /** + * Show a warning message if the user modified a translated field. For each + * field, the notification provides a link to edit the field's translations. + * + * @param {Object[]} alertFields: field list + */ + displayTranslationAlert: function (alertFields) { + this.$('.o_notification_box').remove(); + var $notification = $(qweb.render('notification-box', {type: 'info'})) + .append(qweb.render('translation-alert', { + fields: alertFields, + lang: _t.database.parameters.name + })); + if (this.$('.o_form_statusbar').length) { + this.$('.o_form_statusbar').after($notification); + } else { + this.$el.prepend($notification); + } + }, /** * @see BasicRenderer.confirmChange * @@ -771,6 +793,16 @@ var FormRenderer = BasicRenderer.extend({ this._activatePreviousFieldWidget(this.state, index); } }, + /** + * open the translation view for the current field + * + * @private + * @param {MouseEvent} ev + */ + _onTranslate: function (event) { + event.preventDefault(); + this.trigger_up('translate', {fieldName: event.target.name, id: this.state.id}); + }, }); return FormRenderer; 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 9e5e0fc1267..bdf58858b02 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -127,7 +127,7 @@ var KanbanModel = BasicModel.extend({ } return this.notifyChanges(recordID, changes).then(function () { return self.save(recordID); - }).then(function (result) { + }).then(function () { // Remove record from its current group var old_group; for (var i = 0; i < parent.count; i++) { @@ -140,7 +140,7 @@ var KanbanModel = BasicModel.extend({ } } // Add record to its new group - new_group.data.push(result.id); + new_group.data.push(recordID); new_group.count++; return [old_group.id, new_group.id]; }); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 96a0b809441..b9b7caeea7d 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1219,7 +1219,7 @@ -
+
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 8417e503676..2c1d0520748 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -662,7 +662,7 @@ QUnit.module('basic_fields', { this.data.partner.fields.foo.translate = true; - var multi_lang = _t.database.multi_lang; + var multiLang = _t.database.multi_lang; _t.database.multi_lang = true; var form = createView({ @@ -707,7 +707,7 @@ QUnit.module('basic_fields', { assert.strictEqual($button.length, 0, "should not have a translate button in create mode"); form.destroy(); - _t.database.multi_lang = multi_lang; + _t.database.multi_lang = multiLang; }); @@ -875,7 +875,7 @@ QUnit.module('basic_fields', { this.data.partner.fields.txt.translate = true; - var multi_lang = _t.database.multi_lang; + var multiLang = _t.database.multi_lang; _t.database.multi_lang = true; var form = createView({ @@ -920,7 +920,7 @@ QUnit.module('basic_fields', { assert.strictEqual($button.length, 0, "should not have a translate button in create mode"); form.destroy(); - _t.database.multi_lang = multi_lang; + _t.database.multi_lang = multiLang; }); QUnit.module('FieldBinary'); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 52129f23f32..6754f9c2eb5 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -4377,5 +4377,38 @@ QUnit.module('Views', { form.destroy(); }); + + QUnit.test('display translation alert', function (assert) { + assert.expect(1); + + this.data.partner.fields.foo.translate = true; + + var multi_lang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="foo"]').val("test").trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert"); + + form.destroy(); + + _t.database.multi_lang = multi_lang; + }); }); });