diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 7a7e94d2f5..44153f7e4a 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -20,7 +20,21 @@ context('Control Link', () => { 'label': 'Select ToDo', 'fieldname': 'link', 'fieldtype': 'Link', - 'options': 'ToDo' + 'options': 'ToDo', + } + ] + }); + } + + function get_dialog_with_user_link() { + return cy.dialog({ + title: 'Link', + fields: [ + { + 'label': 'Select User', + 'fieldname': 'link', + 'fieldtype': 'Link', + 'options': 'User', } ] }); @@ -29,6 +43,24 @@ context('Control Link', () => { it('should set the valid value', () => { get_dialog_with_link().as('dialog'); + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "User", + "property": "translate_link_fields", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "0" + }, true); + + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "ToDo", + "property": "show_title_field_in_link", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "0" + }, true); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); @@ -88,7 +120,8 @@ context('Control Link', () => { cy.get('@input').type(todos[0]).blur(); cy.wait('@validate_link'); cy.get('@input').focus(); - cy.findByTitle('Open Link') + cy.wait(500); // wait for arrow to show + cy.get('.frappe-control[data-fieldname=link] .btn-open') .should('be.visible') .click(); cy.location('pathname').should('eq', `/app/todo/${todos[0]}`); @@ -96,7 +129,15 @@ context('Control Link', () => { }); it('show title field in link', () => { - get_dialog_with_link().as('dialog'); + + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "User", + "property": "translate_link_fields", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "0" + }, true); cy.insert_doc("Property Setter", { "doctype": "Property Setter", @@ -107,6 +148,10 @@ context('Control Link', () => { "value": "1" }, true); + cy.clear_cache(); + cy.wait(500); + + get_dialog_with_link().as('dialog'); cy.window().its('frappe').then(frappe => { if (!frappe.boot) { frappe.boot = { @@ -134,8 +179,6 @@ context('Control Link', () => { expect(value).to.eq(todos[0]); expect(label).to.eq('this is a test todo for link'); - - cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link"); }); }); }); @@ -143,6 +186,7 @@ context('Control Link', () => { it('should update dependant fields (via fetch_from)', () => { cy.get('@todos').then(todos => { cy.visit(`/app/todo/${todos[0]}`); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link'); cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input'); @@ -167,7 +211,9 @@ context('Control Link', () => { .should("eq", null); // set valid value again - cy.get('@input').clear().type('Administrator', {delay: 100}).blur(); + cy.get('@input').clear().focus(); + cy.wait('@search_link'); + cy.get('@input').type('Administrator', {delay: 100}).blur(); cy.wait('@validate_link'); cy.window() @@ -214,4 +260,130 @@ context('Control Link', () => { "contain", "" ); }); + + it('show translated text for link with show_title_field_in_link enabled', () => { + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "ToDo", + "property": "translate_link_fields", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "1" + }, true); + + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "ToDo", + "property": "show_title_field_in_link", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "1" + }, true); + + cy.window().its('frappe').then(frappe => { + cy.insert_doc("Translation", { + doctype: "Translation", + language: frappe.boot.lang, + source_text: "this is a test todo for link", + translated_text: "this is a translated test todo for link", + }); + }); + + cy.clear_cache(); + cy.wait(500); + + cy.window().its('frappe').then(frappe => { + if (!frappe.boot) { + frappe.boot = { + link_title_doctypes: ['ToDo'], + translatable_doctypes: ['ToDo'] + }; + } else { + frappe.boot.link_title_doctypes = ['ToDo']; + frappe.boot.translatable_doctypes = ['ToDo']; + } + }); + + get_dialog_with_link().as('dialog'); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + + cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); + cy.wait('@search_link'); + cy.get('@input').type('todo for link', { delay: 100 }); + cy.wait('@search_link'); + cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible'); + cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=link] input').blur(); + cy.get('@dialog').then(dialog => { + cy.get('@todos').then(todos => { + let field = dialog.get_field('link'); + let value = field.get_value(); + let label = field.get_label_value(); + + expect(value).to.eq(todos[0]); + expect(label).to.eq('this is a translated test todo for link'); + }); + }); + }); + + it('show translated text for link with show_title_field_in_link disabled', () => { + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "User", + "property": "translate_link_fields", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "1" + }, true); + + cy.insert_doc("Property Setter", { + "doctype": "Property Setter", + "doc_type": "ToDo", + "property": "show_title_field_in_link", + "property_type": "Check", + "doctype_or_field": "DocType", + "value": "0" + }, true); + + cy.window().its('frappe').then(frappe => { + cy.insert_doc("Translation", { + doctype: "Translation", + language: frappe.boot.lang, + source_text: "test@erpnext.com", + translated_text: "translatedtest@erpnext.com", + }); + }); + + cy.clear_cache(); + cy.wait(500); + + cy.window().its('frappe').then(frappe => { + if (!frappe.boot) { + frappe.boot = { + translatable_doctypes: ['User'] + }; + } else { + frappe.boot.translatable_doctypes = ['User']; + } + }); + + get_dialog_with_user_link().as('dialog'); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + + cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); + cy.wait('@search_link'); + cy.get('@input').type('test@erpnext.com', { delay: 100 }); + cy.wait('@search_link'); + cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible'); + cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=link] input').blur(); + cy.get('@dialog').then(dialog => { + let field = dialog.get_field('link'); + let value = field.get_value(); + let label = field.get_label_value(); + + expect(value).to.eq('test@erpnext.com'); + expect(label).to.eq('translatedtest@erpnext.com'); + }); + }); }); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index f873461efb..ae93354964 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -48,4 +48,4 @@ context('Table MultiSelect', () => { cy.get('@existing_value').find('.btn-link-to-form').click(); cy.location('pathname').should('contain', '/user/test@erpnext.com'); }); -}); +}); \ No newline at end of file diff --git a/frappe/boot.py b/frappe/boot.py index 62122ed4e5..a23a7e6ac3 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -100,6 +100,7 @@ def get_bootinfo(): bootinfo.desk_settings = get_desk_settings() bootinfo.app_logo_url = get_app_logo() bootinfo.link_title_doctypes = get_link_title_doctypes() + bootinfo.translatable_doctypes = get_translatable_doctypes() return bootinfo @@ -408,3 +409,11 @@ def set_time_zone(bootinfo): "user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone(), } + + +def get_translatable_doctypes(): + dts = frappe.get_all("DocType", {"translate_link_fields": 1}, pluck="name") + custom_dts = frappe.get_all( + "Property Setter", {"property": "translate_link_fields", "value": "1"}, pluck="doc_type" + ) + return dts + custom_dts diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index b1579f35cd..4e110202d2 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -47,6 +47,7 @@ "view_settings", "title_field", "show_title_field_in_link", + "translate_link_fields", "search_fields", "default_print_format", "sort_field", @@ -591,6 +592,12 @@ "fieldname": "show_title_field_in_link", "fieldtype": "Check", "label": "Show Title in Link Fields" + }, + { + "default": "0", + "fieldname": "translate_link_fields", + "fieldtype": "Check", + "label": "Translate Link Fields" } ], "icon": "fa fa-bolt", @@ -673,7 +680,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2022-02-15 21:47:16.467217", + "modified": "2022-02-28 21:56:52.116915", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -708,5 +715,6 @@ "sort_field": "modified", "sort_order": "DESC", "states": [], - "track_changes": 1 + "track_changes": 1, + "translate_link_fields": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4ce2c73fa3..3ec6795f0e 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -16,7 +16,6 @@ frappe.ui.form.on("Customize Form", { onload: function(frm) { frm.set_query("doc_type", function() { return { - translate_values: false, filters: [ ["DocType", "issingle", "=", 0], ["DocType", "custom", "=", 0], diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 4c40b80f53..0011f51af4 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -29,6 +29,7 @@ "view_settings_section", "title_field", "show_title_field_in_link", + "translate_link_fields", "image_field", "default_print_format", "column_break_29", @@ -311,6 +312,12 @@ "fieldname": "show_title_field_in_link", "fieldtype": "Check", "label": "Show Title in Link Fields" + }, + { + "default": "0", + "fieldname": "translate_link_fields", + "fieldtype": "Check", + "label": "Translate Link Fields" } ], "hide_toolbar": 1, @@ -319,7 +326,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-12 15:36:16.772277", + "modified": "2022-05-13 15:36:16.772277", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 903d7fc27d..e92fd50ea8 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -577,6 +577,7 @@ doctype_properties = { "naming_rule": "Data", "autoname": "Data", "show_title_field_in_link": "Check", + "translate_link_fields": "Check", } docfield_properties = { diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index f2a1206c7c..dc91873a82 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -226,6 +226,7 @@ CREATE TABLE `tabDocType` ( `sender_field` varchar(255) DEFAULT NULL, `show_title_field_in_link` int(1) NOT NULL DEFAULT 0, `migration_hash` varchar(255) DEFAULT NULL, + `translate_link_fields` int(1) NOT NULL DEFAULT 0, PRIMARY KEY (`name`) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 2cae3ab82f..99e94a226f 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -231,6 +231,7 @@ CREATE TABLE "tabDocType" ( "sender_field" varchar(255) DEFAULT NULL, "show_title_field_in_link" smallint NOT NULL DEFAULT 0, "migration_hash" varchar(255) DEFAULT NULL, + "translate_link_fields" smallint NOT NULL DEFAULT 0, PRIMARY KEY ("name") ) ; diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 2081a301c3..34b863d54f 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -36,6 +36,9 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat if(!me.$input.val()) { me.$input.val("").trigger("input"); + + // hide link arrow to doctype if none is set + me.$link.toggle(false); } }, 500); }); @@ -78,6 +81,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat } this.set_link_title(value); } + get_translated(value) { + return this.is_translatable() ? __(value) : value; + } + is_translatable() { + return in_list(frappe.boot?.translatable_doctypes || [], this.get_options()); + } set_link_title(value) { let doctype = this.get_options(); @@ -89,25 +98,32 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat link_title = frappe.utils .fetch_link_title(doctype, value) .then(link_title => { - this.set_input_value(link_title); - this.title_value_map[link_title] = value; + this.translate_and_set_input_value(link_title, value); }); } else { - this.set_input_value(link_title); - this.title_value_map[link_title] = value; + this.translate_and_set_input_value(link_title, value); } } else { - this.set_input_value(value); + this.translate_and_set_input_value(value, value) } } + translate_and_set_input_value(link_title, value) { + let translated_link_text = this.get_translated(link_title) + this.title_value_map[translated_link_text] = value; + + this.set_input_value(translated_link_text); + } parse_validate_and_set_in_model(value, e, label) { if (this.parse) value = this.parse(value, label); if (label) { - this.label = label; + this.label = this.get_translated(label); frappe.utils.add_link_title(this.df.options, value, label); } - return this.validate_and_set_in_model(value, e); + return this.validate_and_set_in_model(value, e, true); + } + parse(value) { + return strip_html(value); } get_input_value() { if (this.$input) { @@ -164,7 +180,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat return false; } setup_awesomeplete() { - var me = this; + let me = this; this.$input.cache = {}; @@ -173,14 +189,14 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat maxItems: 99, autoFirst: true, list: [], - replace: function (suggestion) { + replace: function (item) { // Override Awesomeplete replace function as it is used to set the input value // https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403 - this.input.value = suggestion.label || suggestion.value; + this.input.value = me.get_translated(item.label || item.value); }, data: function (item) { return { - label: item.label || item.value, + label: me.get_translated(item.label || item.value), value: item.value }; }, @@ -188,11 +204,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat return true; }, item: function (item) { - var d = this.get_item(item.value); + let d = this.get_item(item.value); if(!d.label) { d.label = d.value; } - var _label = (me.translate_values) ? __(d.label) : d.label; - var html = d.html || "" + _label + ""; + let _label = me.get_translated(d.label); + let html = d.html || "" + _label + ""; if(d.description && d.value!==d.description) { html += '
' + __(d.description) + ''; } @@ -304,10 +320,20 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat this.$input.on("awesomplete-open", () => { this.autocomplete_open = true; + + if (!me.get_label_value()) { + // hide link arrow to doctype if none is set + me.$link.toggle(false); + } }); - this.$input.on("awesomplete-close", () => { + this.$input.on("awesomplete-close", (e) => { this.autocomplete_open = false; + + if (!me.get_label_value()) { + // hide link arrow to doctype if none is set + me.$link.toggle(false); + } }); this.$input.on("awesomplete-select", function(e) { @@ -317,7 +343,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat me.autocomplete_open = false; // prevent selection on tab - var TABKEY = 9; + let TABKEY = 9; if (e.keyCode === TABKEY) { e.preventDefault(); me.awesomplete.close(); @@ -347,6 +373,24 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat me.$input.val(""); } }); + + this.$input.on("focus", function () { + if (!frappe.boot.translated_search_doctypes.includes(me.df.options)) { + me.show_untranslated(); + } + }); + + this.$input.keydown((e) => { + let BACKSPACE = 8; + if (e.keyCode === BACKSPACE && !frappe.boot.translated_search_doctypes.includes(me.df.options)) { + me.show_untranslated(); + } + }); + } + + show_untranslated() { + let value = this.get_input_value(); + this.is_translatable() && this.set_input_value(value); } merge_duplicates(results) { @@ -590,5 +634,4 @@ if (Awesomplete) { return item.value === value; }); }; -} - +} \ No newline at end of file diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index 477679bc92..e106d8eed6 100644 --- a/frappe/public/js/frappe/form/controls/table_multiselect.js +++ b/frappe/public/js/frappe/form/controls/table_multiselect.js @@ -161,4 +161,14 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f return true; }; } + get_input_value() { + return this.$input ? this.$input.val() : undefined; + } + update_value() { + let value = this.get_input_value(); + + if (value !== this.last_value) { + this.parse_validate_and_set_in_model(value); + } + } }; diff --git a/frappe/sessions.py b/frappe/sessions.py index c07bd7495b..6e0ce73732 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -184,6 +184,7 @@ def get(): frappe.get_attr(hook)(bootinfo=bootinfo) bootinfo["lang"] = frappe.translate.get_user_lang() + bootinfo["translated_search_doctypes"] = frappe.get_hooks("translated_search_doctypes") bootinfo["disable_async"] = frappe.conf.disable_async bootinfo["setup_complete"] = cint(frappe.db.get_single_value("System Settings", "setup_complete"))