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