summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorVincent Schippefilt <vsc@odoo.com>2019-08-20 07:46:52 +0000
committerVincent Schippefilt <vsc@odoo.com>2019-09-09 13:09:53 +0000
commit029dd1a214f727e25b479610df96b7ee94dd8127 (patch)
tree5b33cb401962523f20c768043def21a7b0349cfc /addons/web/static/src/js
parent263849c3365af3d62fa5b9ceaff65b2f7b84c092 (diff)
[IMP] web: edit translations in new dialog
Before this commit, translations were edited in a list view with a filter on the field that needed to be translated. After this commit, the translations for each fields are translated in a dedicated dialog. The changes done to the current language are taken into account immediately, both ways, from the dialog to the form and from the form to the dialog. Translating terms is also available in create mode, the user will be prompted to save before editing a translation Task ID: 2028152 closes odoo/odoo#36185 Signed-off-by: VincentSchippefilt <VincentSchippefilt@users.noreply.github.com>
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js33
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js53
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js6
-rw-r--r--addons/web/static/src/js/widgets/translation_dialog.js172
4 files changed, 237 insertions, 27 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index bc4b4e6b7d3..da46488cffa 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -40,13 +40,9 @@ var TranslatableFieldMixin = {
* @returns {jQuery}
*/
_renderTranslateButton: function () {
- if (_t.database.multi_lang && this.field.translate && this.res_id) {
+ if (_t.database.multi_lang && this.field.translate) {
var lang = _t.database.parameters.code.split('_')[0].toUpperCase();
- return $('<button>', {
- type: 'button',
- 'class': 'o_field_translate fa fa-globe btn btn-link',
- 'data-code': lang,
- })
+ return $(`<span class="o_field_translate btn btn-link">${lang}</span>`)
.on('click', this._onTranslate.bind(this));
}
return $();
@@ -59,10 +55,16 @@ var TranslatableFieldMixin = {
/**
* open the translation view for the current field
*
+ * @param {MouseEvent} ev
* @private
*/
- _onTranslate: function () {
- this.trigger_up('translate', {fieldName: this.name, id: this.dataPointID});
+ _onTranslate: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('translate', {
+ fieldName: this.name,
+ id: this.dataPointID,
+ isComingFromTranslationAlert: false,
+ });
},
};
@@ -213,7 +215,9 @@ var InputField = DebouncedField.extend({
if (!event || event === this.lastChangeEvent) {
this.isDirty = false;
}
- if (this.isDirty || (event && event.target === this && event.data.changes[this.name] === this.value)) {
+ if (this.isDirty || (event && event.target === this &&
+ event.data.changes &&
+ event.data.changes[this.name] === this.value)) {
if (this.attrs.decorations) {
// if a field is modified, then it could have triggered an onchange
// which changed some of its decorations. Since we bypass the
@@ -498,7 +502,10 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
if (this.field.size && this.field.size > 0) {
this.$el.attr('maxlength', this.field.size);
}
- this.$el = this.$el.add(this._renderTranslateButton());
+ if (this.field.translate) {
+ this.$el = this.$el.add(this._renderTranslateButton());
+ this.$el.addClass('o_field_translate');
+ }
return def;
},
/**
@@ -1327,8 +1334,10 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
start: function () {
if (this.mode === 'edit') {
dom.autoresize(this.$el, this.autoResizeOptions);
-
- this.$el = this.$el.add(this._renderTranslateButton());
+ if (this.field.translate) {
+ this.$el = this.$el.add(this._renderTranslateButton());
+ this.$el.addClass('o_field_translate');
+ }
}
return this._super();
},
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 2163f074cb0..be0b9a00bf3 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -12,6 +12,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var FieldManagerMixin = require('web.FieldManagerMixin');
var Pager = require('web.Pager');
+var TranslationDialog = require('web.TranslationDialog');
var _t = core._t;
@@ -332,6 +333,23 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return this.renderer.confirmChange(state, id, fields, e);
},
/**
+ * Ask the user to confirm he wants to save the record
+ * @private
+ */
+ _confirmSaveNewRecord: function () {
+ var self = this;
+ var def = new Promise(function (resolve, reject) {
+ var message = _t("You need to save this new record before editing the translation. Do you want to proceed?");
+ var dialog = Dialog.confirm(self, message, {
+ title: _t("Warning"),
+ confirm_callback: resolve.bind(self, true),
+ cancel_callback: reject,
+ });
+ dialog.on('closed', self, reject);
+ });
+ return def;
+ },
+ /**
* Delete records (and ask for confirmation if necessary)
*
* @param {string[]} ids list of local record ids
@@ -789,28 +807,35 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private
* @param {OdooEvent} ev
*/
- _onTranslate: function (ev) {
+ _onTranslate: async function (ev) {
ev.stopPropagation();
- var self = this;
- var record = this.model.get(ev.data.id, {raw: true});
- this._rpc({
+
+ if (this.model.isNew(ev.data.id)) {
+ await this._confirmSaveNewRecord();
+ var updatedFields = await this.saveRecord(ev.data.id, { stayInEdit: true });
+ await this._confirmChange(ev.data.id, updatedFields, ev);
+ }
+ var record = this.model.get(ev.data.id, { raw: true });
+ var result = await this._rpc({
route: '/web/dataset/call_button',
params: {
model: 'ir.translation',
method: 'translate_fields',
args: [record.model, record.res_id, ev.data.fieldName],
- kwargs: {context: record.getContext()},
+ kwargs: { context: record.getContext() },
}
- }).then(function (result) {
- self.do_action(result, {
- on_reverse_breadcrumb: function () {
- if (!_.isEmpty(self.renderer.alertFields)) {
- self.renderer.displayTranslationAlert();
- }
- return false;
- },
- });
});
+
+ this.translationDialog = new TranslationDialog(this, {
+ domain: result.domain,
+ searchName: result.context.search_default_name,
+ fieldName: record.fieldsInfo[record.viewType][ev.data.fieldName].name,
+ userLanguageValue: ev.target.value || '',
+ dataPointID: record.id,
+ isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert,
+ isText: record.fields[ev.data.fieldName].type === 'text',
+ });
+ return this.translationDialog.open();
},
});
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 decea8867fc..e1395eebb9a 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -1083,7 +1083,11 @@ var FormRenderer = BasicRenderer.extend({
*/
_onTranslate: function (ev) {
ev.preventDefault();
- this.trigger_up('translate', {fieldName: ev.target.name, id: this.state.id});
+ this.trigger_up('translate', {
+ fieldName: ev.target.name,
+ id: this.state.id,
+ isComingFromTranslationAlert: true,
+ });
},
/**
* remove alert fields of record from alertFields object
diff --git a/addons/web/static/src/js/widgets/translation_dialog.js b/addons/web/static/src/js/widgets/translation_dialog.js
new file mode 100644
index 00000000000..39d841a48d0
--- /dev/null
+++ b/addons/web/static/src/js/widgets/translation_dialog.js
@@ -0,0 +1,172 @@
+odoo.define('web.TranslationDialog', function (require) {
+ 'use strict';
+
+ var core = require('web.core');
+ var Dialog = require('web.Dialog');
+ var session = require('web.session');
+
+ var _t = core._t;
+
+ var TranslationDialog = Dialog.extend({
+ xmlDependencies: (Dialog.prototype.xmlDependencies || [])
+ .concat(['/web/static/src/xml/translation_dialog.xml']),
+ template: 'TranslationDialog',
+
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {Object} [options]
+ * @param {string} [options.domain] the domain needed to get the translation terms
+ * @param {string} [options.fieldName] the name of the field currently translated (from the model of the form view)
+ * @param {string} [options.searchName] the name of the actual field that is the reference for translation (in the form of model,field)
+ * @param {string} [options.userLanguageValue] the value of the translation in the language of the user, as seen in the from view (might be empty)
+ * @param {string} [options.dataPointID] the data point id of the record for which we do the translations
+ * @param {boolean} [options.isComingFromTranslationAlert] the initiator of the dialog, might be a link on a field or the translation alert on top of the form
+ * @param {boolean} [options.isText] is the field a text field (multiline) or char (single line)
+ *
+ */
+ init: function (parent, options) {
+ options = options || {};
+
+ this.fieldName = options.fieldName;
+ this.domain = options.domain;
+ this.searchName = options.searchName;
+ this.userLanguageValue = options.userLanguageValue;
+ this.domain.push(['name', "=", `${this.searchName}`]);
+ this.dataPointID = options.dataPointID;
+ this.isComingFromTranslationAlert = options.isComingFromTranslationAlert;
+ this.currentInterfaceLanguage = session.user_context.lang;
+ this.isText = options.isText;
+
+ this._super(parent, _.extend({
+ size: 'large',
+ title: _t('Translate: ') + `${this.fieldName}`,
+ buttons: [
+ { text: _t('Save'), classes: 'btn-primary', close: true, click: this._onSave.bind(this) },
+ { text: _t('Discard'), close: true },
+ ],
+ }, options));
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ return Promise.all([
+ this._super(),
+ this._loadLanguages().then((l) => {
+ this.languages = l;
+ }),
+ this._loadTranslations().then((t) => {
+ this.translations = t;
+ })
+ ]).then(() => {
+ this.data = this.translations.map((term) => {
+ let relatedLanguage = this.languages.find((language) => language[0] === term.lang);
+ return {
+ id: term.id,
+ lang: term.lang,
+ langName: relatedLanguage[1],
+ // we set the translation value coming from the database, except for the language
+ // the user is currently utilizing. Then we set the translation value coming
+ // from the value of the field in the form
+ value: (term.lang === this.currentInterfaceLanguage &&
+ !this.isComingFromTranslationAlert) ?
+ this.userLanguageValue : term.value || ''
+ };
+ });
+ this.data.sort((left, right) => left.langName < right.langName ? -1 : 1);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Load the translation terms for the installed language, for the current model and res_id
+ * @private
+ */
+ _loadTranslations: function () {
+ return this._rpc({
+ model: 'ir.translation',
+ method: 'search_read',
+ fields: ['lang', 'value'],
+ domain: this.domain,
+ });
+ },
+ /**
+ * Load the installed languages long names and code
+ *
+ * The result of the call is put in cache on the prototype of this dialog.
+ * If any new language is installed, a full page refresh will happen,
+ * so there is no need invalidate it.
+ * @private
+ */
+ _loadLanguages: function () {
+ if (TranslationDialog.prototype.installedLanguagesCache)
+ return Promise.resolve(TranslationDialog.prototype.installedLanguagesCache);
+
+ return this._rpc({
+ model: 'res.lang',
+ method: 'get_installed',
+ fields: ['code', 'name', 'iso_code'],
+ }).then((installedLanguages) => {
+ TranslationDialog.prototype.installedLanguagesCache = installedLanguages;
+ return TranslationDialog.prototype.installedLanguagesCache
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * Save all the terms that have been updated
+ * @private
+ * @returns a promise that is resolved when all the save have occured
+ */
+ _onSave: function () {
+ var updatedTerm = {};
+ var updateFormViewField;
+
+ this.el.querySelectorAll('input[type=text],textarea').forEach((t) => {
+ var initialValue = this.data.find((d) => d.id == t.dataset.id);
+ if (initialValue.value !== t.value) {
+ updatedTerm[t.dataset.id] = t.value;
+
+ if (initialValue.lang === this.currentInterfaceLanguage) {
+ // when the user has changed the term for the language he is
+ // using in the interface, this change should be reflected
+ // in the form view
+ var changes = {};
+ changes[this.fieldName] = updatedTerm[initialValue.id];
+ updateFormViewField = {
+ dataPointID: this.dataPointID,
+ changes: changes,
+ doNotSetDirty: false,
+ };
+ }
+ }
+ });
+
+ // updatedTerm only contains the id and values of the terms that
+ // have been updated by the user
+ var saveUpdatedTermsProms = Object.keys(updatedTerm).map((id) => {
+ var writeTranslation = {
+ model: 'ir.translation',
+ method: 'write',
+ context: this.context,
+ args: [[parseInt(id, 10)], { value: updatedTerm[id] }]
+ };
+ return this._rpc(writeTranslation);
+ });
+ return Promise.all(saveUpdatedTermsProms).then(() => {
+ // we might have to update the value of the field on the form
+ // view that opened the translation dialog
+ if (updateFormViewField) {
+ this.trigger_up('field_changed', updateFormViewField);
+ }
+ });
+ }
+ });
+
+ return TranslationDialog;
+});