Bläddra i källkod

fix: set translated text to link field (#15451)

- 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=fdafee2715
version-14
Himanshu 3 år sedan
committed by GitHub
förälder
incheckning
70409a3c7b
Ingen känd nyckel hittad för denna signaturen i databasen GPG-nyckel ID: 4AEE18F83AFDEB23
12 ändrade filer med 281 tillägg och 29 borttagningar
  1. +178
    -6
      cypress/integration/control_link.js
  2. +1
    -1
      cypress/integration/table_multiselect.js
  3. +9
    -0
      frappe/boot.py
  4. +10
    -2
      frappe/core/doctype/doctype/doctype.json
  5. +0
    -1
      frappe/custom/doctype/customize_form/customize_form.js
  6. +8
    -1
      frappe/custom/doctype/customize_form/customize_form.json
  7. +1
    -0
      frappe/custom/doctype/customize_form/customize_form.py
  8. +1
    -0
      frappe/database/mariadb/framework_mariadb.sql
  9. +1
    -0
      frappe/database/postgres/framework_postgres.sql
  10. +61
    -18
      frappe/public/js/frappe/form/controls/link.js
  11. +10
    -0
      frappe/public/js/frappe/form/controls/table_multiselect.js
  12. +1
    -0
      frappe/sessions.py

+ 178
- 6
cypress/integration/control_link.js Visa fil

@@ -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');
});
});
});

+ 1
- 1
cypress/integration/table_multiselect.js Visa fil

@@ -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');
});
});
});

+ 9
- 0
frappe/boot.py Visa fil

@@ -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

+ 10
- 2
frappe/core/doctype/doctype/doctype.json Visa fil

@@ -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
}

+ 0
- 1
frappe/custom/doctype/customize_form/customize_form.js Visa fil

@@ -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],


+ 8
- 1
frappe/custom/doctype/customize_form/customize_form.json Visa fil

@@ -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",


+ 1
- 0
frappe/custom/doctype/customize_form/customize_form.py Visa fil

@@ -577,6 +577,7 @@ doctype_properties = {
"naming_rule": "Data",
"autoname": "Data",
"show_title_field_in_link": "Check",
"translate_link_fields": "Check",
}

docfield_properties = {


+ 1
- 0
frappe/database/mariadb/framework_mariadb.sql Visa fil

@@ -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;



+ 1
- 0
frappe/database/postgres/framework_postgres.sql Visa fil

@@ -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")
) ;



+ 61
- 18
frappe/public/js/frappe/form/controls/link.js Visa fil

@@ -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;
});
};
}

}

+ 10
- 0
frappe/public/js/frappe/form/controls/table_multiselect.js Visa fil

@@ -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);
}
}
};

+ 1
- 0
frappe/sessions.py Visa fil

@@ -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"))


Laddar…
Avbryt
Spara