- Fixes [Issue/15324](https://github.com/frappe/frappe/issues/15324) - When selecting a value for Link Field, the English text was set as link field value. - This PR aims to fix it by setting the translated text as link field value on selection. Todo: - [x] Show translated text in the select dropdown - [x] Set translated value to Link field on select - [x] Show original value when link field is in focus - [x] Add option to toggle this behaviour ### Behaviour - Link field loses focus: show the translated name. - Link field is focused - If old value is present - If options **are** one of [Role, DocType]: show the translated name - Else: show the name (untranslated) to enable search in untranslated values - Else: show what the user typed (untranslated) to enable search in untranslated values - Value is selected: link field loses focus ## Demo ### Link to UOM in a custom Item DocType The UOM names are in english, so the search needs to happen in english. When possible, the translation is displayed. #### Before https://user-images.githubusercontent.com/14891507/156415248-e5e80d05-53dc-4ca8-89c7-998986ff6e99.mov #### After https://user-images.githubusercontent.com/14891507/156410386-a874430c-f340-43ed-9c3a-92e8d4d50fc9.mov ### Link to DocType in Customize Form The DocType names get translated before being searched. This is a preexisting hack in the framework for DocType and Role. In this case, we can search in the translations. #### Before https://user-images.githubusercontent.com/14891507/156414648-8e505f8c-9dee-4358-8182-3b358c28bb62.mov #### After https://user-images.githubusercontent.com/14891507/156411881-c4ca22e1-1397-4e13-9768-5e16b72f8d6d.mov https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext/customize-form/edit?wiki_page_patch=fdafee2715version-14
@@ -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'); | |||
}); | |||
}); | |||
}); |
@@ -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'); | |||
}); | |||
}); | |||
}); |
@@ -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 |
@@ -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 | |||
} |
@@ -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], | |||
@@ -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", | |||
@@ -577,6 +577,7 @@ doctype_properties = { | |||
"naming_rule": "Data", | |||
"autoname": "Data", | |||
"show_title_field_in_link": "Check", | |||
"translate_link_fields": "Check", | |||
} | |||
docfield_properties = { | |||
@@ -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; | |||
@@ -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") | |||
) ; | |||
@@ -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 || "<strong>" + _label + "</strong>"; | |||
let _label = me.get_translated(d.label); | |||
let html = d.html || "<strong>" + _label + "</strong>"; | |||
if(d.description && d.value!==d.description) { | |||
html += '<br><span class="small">' + __(d.description) + '</span>'; | |||
} | |||
@@ -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; | |||
}); | |||
}; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
}; |
@@ -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")) | |||