feat: Show Title Field in Link Fieldsversion-14
@@ -95,6 +95,51 @@ 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": "ToDo", | |||
"property": "show_title_field_in_link", | |||
"property_type": "Check", | |||
"doctype_or_field": "DocType", | |||
"value": "1" | |||
}, true); | |||
cy.window().its('frappe').then(frappe => { | |||
if (!frappe.boot) { | |||
frappe.boot = { | |||
link_title_doctypes: ['ToDo'] | |||
}; | |||
} else { | |||
frappe.boot.link_title_doctypes = ['ToDo']; | |||
} | |||
}); | |||
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'); | |||
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 test todo for link'); | |||
cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link"); | |||
}); | |||
}); | |||
}); | |||
it('should update dependant fields (via fetch_from)', () => { | |||
cy.get('@todos').then(todos => { | |||
cy.visit(`/app/todo/${todos[0]}`); | |||
@@ -89,6 +89,7 @@ def get_bootinfo(): | |||
bootinfo.additional_filters_config = get_additional_filters_from_hooks() | |||
bootinfo.desk_settings = get_desk_settings() | |||
bootinfo.app_logo_url = get_app_logo() | |||
bootinfo.link_title_doctypes = get_link_title_doctypes() | |||
return bootinfo | |||
@@ -324,6 +325,15 @@ def get_desk_settings(): | |||
def get_notification_settings(): | |||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | |||
def get_link_title_doctypes(): | |||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) | |||
custom_dts = frappe.get_all( | |||
"Property Setter", | |||
{"property": "show_title_field_in_link", "value": "1"}, | |||
["doc_type as name"], | |||
) | |||
return [d.name for d in dts + custom_dts if d] | |||
def set_time_zone(bootinfo): | |||
bootinfo.time_zone = { | |||
"system": get_time_zone(), | |||
@@ -46,6 +46,7 @@ | |||
"allow_auto_repeat", | |||
"view_settings", | |||
"title_field", | |||
"show_title_field_in_link", | |||
"search_fields", | |||
"default_print_format", | |||
"sort_field", | |||
@@ -582,6 +583,12 @@ | |||
"fieldname": "document_states_section", | |||
"fieldtype": "Section Break", | |||
"label": "Document States" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "show_title_field_in_link", | |||
"fieldtype": "Check", | |||
"label": "Show Title in Link Fields" | |||
} | |||
], | |||
"icon": "fa fa-bolt", | |||
@@ -663,7 +670,7 @@ | |||
"link_fieldname": "reference_doctype" | |||
} | |||
], | |||
"modified": "2021-12-09 14:53:10.717788", | |||
"modified": "2022-01-07 16:07:06.196534", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "DocType", | |||
@@ -27,6 +27,7 @@ | |||
"autoname", | |||
"view_settings_section", | |||
"title_field", | |||
"show_title_field_in_link", | |||
"image_field", | |||
"default_print_format", | |||
"column_break_29", | |||
@@ -296,6 +297,12 @@ | |||
"fieldtype": "Table", | |||
"label": "States", | |||
"options": "DocType State" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "show_title_field_in_link", | |||
"fieldtype": "Check", | |||
"label": "Show Title in Link Fields" | |||
} | |||
], | |||
"hide_toolbar": 1, | |||
@@ -304,7 +311,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2021-12-14 16:45:04.308690", | |||
"modified": "2022-01-07 16:07:06.196534", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Customize Form", | |||
@@ -516,7 +516,8 @@ doctype_properties = { | |||
'email_append_to': 'Check', | |||
'subject_field': 'Data', | |||
'sender_field': 'Data', | |||
'autoname': 'Data' | |||
'autoname': 'Data', | |||
'show_title_field_in_link': 'Check' | |||
} | |||
docfield_properties = { | |||
@@ -224,6 +224,7 @@ CREATE TABLE `tabDocType` ( | |||
`email_append_to` int(1) NOT NULL DEFAULT 0, | |||
`subject_field` varchar(255) DEFAULT NULL, | |||
`sender_field` varchar(255) DEFAULT NULL, | |||
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0, | |||
`migration_hash` varchar(255) DEFAULT NULL, | |||
PRIMARY KEY (`name`) | |||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||
@@ -229,6 +229,7 @@ CREATE TABLE "tabDocType" ( | |||
"email_append_to" smallint NOT NULL DEFAULT 0, | |||
"subject_field" varchar(255) DEFAULT NULL, | |||
"sender_field" varchar(255) DEFAULT NULL, | |||
"show_title_field_in_link" smallint NOT NULL DEFAULT 0, | |||
"migration_hash" varchar(255) DEFAULT NULL, | |||
PRIMARY KEY ("name") | |||
) ; | |||
@@ -49,7 +49,7 @@ def getdoc(doctype, name, user=None): | |||
raise | |||
doc.add_seen() | |||
set_link_titles(doc) | |||
frappe.response.docs.append(doc) | |||
@frappe.whitelist() | |||
@@ -367,6 +367,60 @@ def get_additional_timeline_content(doctype, docname): | |||
return contents | |||
def set_link_titles(doc): | |||
link_titles = {} | |||
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc)) | |||
link_titles.update(get_title_values_for_table_and_multiselect_fields(doc)) | |||
send_link_titles(link_titles) | |||
def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None): | |||
link_titles = {} | |||
if not link_fields: | |||
meta = frappe.get_meta(doc.doctype) | |||
link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields() | |||
for field in link_fields: | |||
if not doc.get(field.fieldname): | |||
continue | |||
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options) | |||
meta = frappe.get_meta(doctype) | |||
if not meta or not (meta.title_field and meta.show_title_field_in_link): | |||
continue | |||
link_title = frappe.db.get_value( | |||
doctype, doc.get(field.fieldname), meta.title_field, cache=True | |||
) | |||
link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title}) | |||
return link_titles | |||
def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None): | |||
link_titles = {} | |||
if not table_fields: | |||
meta = frappe.get_meta(doc.doctype) | |||
table_fields = meta.get_table_fields() | |||
for field in table_fields: | |||
if not doc.get(field.fieldname): | |||
continue | |||
for value in doc.get(field.fieldname): | |||
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value)) | |||
return link_titles | |||
def send_link_titles(link_titles): | |||
"""Append link titles dict in `frappe.local.response`.""" | |||
if "_link_titles" not in frappe.local.response: | |||
frappe.local.response["_link_titles"] = {} | |||
frappe.local.response["_link_titles"].update(link_titles) | |||
def update_user_info(docinfo): | |||
for d in docinfo.communications: | |||
frappe.utils.add_user_info(d.sender, docinfo.user_info) | |||
@@ -387,3 +441,4 @@ def get_user_info_for_viewers(users): | |||
frappe.utils.add_user_info(user, user_info) | |||
return user_info | |||
@@ -49,8 +49,10 @@ def sanitize_searchfield(searchfield): | |||
# this is called by the Link Field | |||
@frappe.whitelist() | |||
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False): | |||
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) | |||
frappe.response['results'] = build_for_autosuggest(frappe.response["values"]) | |||
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, | |||
reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) | |||
frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype) | |||
del frappe.response["values"] | |||
# this is called by the search box | |||
@@ -138,6 +140,12 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||
fields = list(set(fields + json.loads(filter_fields))) | |||
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] | |||
title_field_query = get_title_field_query(meta) | |||
# Insert title field query after name | |||
if title_field_query: | |||
formatted_fields.insert(1, title_field_query) | |||
# find relevance as location of search term from the beginning of string `name`. used for sorting results. | |||
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format( | |||
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype)) | |||
@@ -205,11 +213,38 @@ def get_std_fields_list(meta, key): | |||
return sflist | |||
def build_for_autosuggest(res): | |||
def get_title_field_query(meta): | |||
title_field = meta.title_field if meta.title_field else None | |||
show_title_field_in_link = meta.show_title_field_in_link if meta.show_title_field_in_link else None | |||
field = None | |||
if title_field and show_title_field_in_link: | |||
field = "`tab{0}`.{1} as `label`".format(meta.name, title_field) | |||
return field | |||
def build_for_autosuggest(res, doctype): | |||
results = [] | |||
for r in res: | |||
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])} | |||
results.append(out) | |||
meta = frappe.get_meta(doctype) | |||
if not (meta.title_field and meta.show_title_field_in_link): | |||
for r in res: | |||
r = list(r) | |||
results.append({ | |||
"value": r[0], | |||
"description": ", ".join(unique(cstr(d) for d in r[1:] if d)) | |||
}) | |||
else: | |||
title_field_exists = meta.title_field and meta.show_title_field_in_link | |||
_from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists | |||
for r in res: | |||
r = list(r) | |||
results.append({ | |||
"value": r[0], | |||
"label": r[1] if title_field_exists else None, | |||
"description": ", ".join(unique(cstr(d) for d in r[_from:] if d)) | |||
}) | |||
return results | |||
def scrub_custom_query(query, key, txt): | |||
@@ -272,3 +307,12 @@ def get_user_groups(): | |||
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={ | |||
'is_group': True | |||
}) | |||
@frappe.whitelist() | |||
def get_link_title(doctype, docname): | |||
meta = frappe.get_meta(doctype) | |||
if meta.title_field and meta.show_title_field_in_link: | |||
return frappe.db.get_value(doctype, docname, meta.title_field) | |||
return docname |
@@ -29,7 +29,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
setTimeout(function() { | |||
if(me.$input.val() && me.get_options()) { | |||
let doctype = me.get_options(); | |||
let name = me.$input.val(); | |||
let name = me.get_input_value(); | |||
me.$link.toggle(true); | |||
me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name)); | |||
} | |||
@@ -69,6 +69,59 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
this.$input_area.find(".link-btn").remove(); | |||
} | |||
} | |||
set_formatted_input(value) { | |||
super.set_formatted_input(); | |||
if (!value) return; | |||
if (!this.title_value_map) { | |||
this.title_value_map = {}; | |||
} | |||
this.set_link_title(value); | |||
} | |||
set_link_title(value) { | |||
let doctype = this.get_options(); | |||
if (!doctype) return; | |||
if (in_list(frappe.boot.link_title_doctypes, doctype)) { | |||
let link_title = frappe.utils.get_link_title(doctype, value); | |||
if (!link_title) { | |||
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; | |||
}); | |||
} else { | |||
this.set_input_value(link_title); | |||
this.title_value_map[link_title] = value; | |||
} | |||
} else { | |||
this.set_input_value(value); | |||
} | |||
} | |||
parse_validate_and_set_in_model(value, e, label) { | |||
if (this.parse) value = this.parse(value, label); | |||
if (label) { | |||
this.label = label; | |||
frappe.utils.add_link_title(this.df.options, value, label); | |||
} | |||
return this.validate_and_set_in_model(value, e); | |||
} | |||
get_input_value() { | |||
if (this.$input) { | |||
const input_value = this.$input.val(); | |||
return this.title_value_map?.[input_value] || input_value; | |||
} | |||
return null; | |||
} | |||
get_label_value() { | |||
return this.$input ? this.$input.val() : ""; | |||
} | |||
set_input_value(value) { | |||
this.$input && this.$input.val(value); | |||
} | |||
open_advanced_search() { | |||
var doctype = this.get_options(); | |||
if(!doctype) return; | |||
@@ -98,7 +151,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
} | |||
// partially entered name field | |||
frappe.route_options.name_field = this.get_value(); | |||
frappe.route_options.name_field = this.get_label_value(); | |||
// reference to calling link | |||
frappe._from_link = this; | |||
@@ -120,6 +173,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
maxItems: 99, | |||
autoFirst: true, | |||
list: [], | |||
replace: function (suggestion) { | |||
// 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; | |||
}, | |||
data: function (item) { | |||
return { | |||
label: item.label || item.value, | |||
@@ -236,9 +294,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
me.selected = false; | |||
return; | |||
} | |||
var value = me.get_input_value(); | |||
if(value!==me.last_value) { | |||
me.parse_validate_and_set_in_model(value); | |||
let value = me.get_input_value(); | |||
let label = me.get_label_value(); | |||
if (value !== me.last_value || me.label !== label) { | |||
me.parse_validate_and_set_in_model(value, null, label); | |||
} | |||
}); | |||
@@ -258,14 +318,15 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
// prevent selection on tab | |||
var TABKEY = 9; | |||
if(e.keyCode === TABKEY) { | |||
if (e.keyCode === TABKEY) { | |||
e.preventDefault(); | |||
me.awesomplete.close(); | |||
return false; | |||
} | |||
if(item.action) { | |||
if (item.action) { | |||
item.value = ""; | |||
item.label = ""; | |||
item.action.apply(me); | |||
} | |||
@@ -277,12 +338,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
frappe.boot.user.last_selected_values[me.df.options] = item.value; | |||
} | |||
me.parse_validate_and_set_in_model(item.value); | |||
me.parse_validate_and_set_in_model(item.value, null, item.label); | |||
}); | |||
this.$input.on("awesomplete-selectcomplete", function(e) { | |||
var o = e.originalEvent; | |||
if(o.text.value.indexOf("__link_option") !== -1) { | |||
let o = e.originalEvent; | |||
if (o.text.value.indexOf("__link_option") !== -1) { | |||
me.$input.val(""); | |||
} | |||
}); | |||
@@ -83,15 +83,21 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends f | |||
} | |||
get_pill_html(value) { | |||
const label = this.get_label(value); | |||
const encoded_value = encodeURIComponent(value); | |||
return ` | |||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}"> | |||
<span class="btn-link-to-form">${__(value)}</span> | |||
<span class="btn-link-to-form">${__(label || value)}</span> | |||
<span class="btn-remove">${frappe.utils.icon('close')}</span> | |||
</button> | |||
`; | |||
} | |||
get_label(value) { | |||
const item = this._data?.find(d => d.value === value); | |||
return item ? item.label || item.value : null; | |||
} | |||
get_awesomplete_settings() { | |||
const settings = super.get_awesomplete_settings(); | |||
@@ -49,7 +49,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f | |||
setup_buttons() { | |||
this.$input_area.find('.link-btn').remove(); | |||
} | |||
parse(value) { | |||
parse(value, label) { | |||
const link_field = this.get_link_field(); | |||
if (value) { | |||
@@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f | |||
[link_field.fieldname]: value | |||
}); | |||
} | |||
frappe.utils.add_link_title(link_field.options, value, label); | |||
} | |||
this._rows_list = this.rows.map(row => row[link_field.fieldname]); | |||
return this.rows; | |||
@@ -126,10 +127,12 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f | |||
this.$input_area.prepend(html); | |||
} | |||
get_pill_html(value) { | |||
const link_field = this.get_link_field(); | |||
const encoded_value = encodeURIComponent(value); | |||
const pill_name = frappe.utils.get_link_title(link_field.options, value) || value; | |||
return ` | |||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}"> | |||
<span class="btn-link-to-form">${__(value)}</span> | |||
<span class="btn-link-to-form">${__(pill_name)}</span> | |||
<span class="btn-remove">${frappe.utils.icon('close')}</span> | |||
</button> | |||
`; | |||
@@ -110,12 +110,14 @@ frappe.form.formatters = { | |||
Link: function(value, docfield, options, doc) { | |||
var doctype = docfield._options || docfield.options; | |||
var original_value = value; | |||
let link_title = frappe.utils.get_link_title(doctype, value); | |||
if(value && value.match && value.match(/^['"].*['"]$/)) { | |||
value.replace(/^.(.*).$/, "$1"); | |||
} | |||
if(options && (options.for_print || options.only_value)) { | |||
return value; | |||
return link_title || value; | |||
} | |||
if(frappe.form.link_formatters[doctype]) { | |||
@@ -139,13 +141,14 @@ frappe.form.formatters = { | |||
return `<a | |||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" | |||
data-doctype="${doctype}" | |||
data-name="${original_value}"> | |||
${__(options && options.label || value)}</a>`; | |||
data-name="${original_value}" | |||
data-value="${original_value}"> | |||
${__(options && options.label || link_title || value)}</a>`; | |||
} else { | |||
return value; | |||
return link_title || value; | |||
} | |||
} else { | |||
return value; | |||
return link_title || value; | |||
} | |||
}, | |||
Date: function(value) { | |||
@@ -249,30 +249,39 @@ frappe.ui.form.update_calling_link = (newdoc) => { | |||
}; | |||
if (is_valid_doctype()) { | |||
// set value | |||
if (doc && doc.parentfield) { | |||
//update values for child table | |||
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) { | |||
if (field.doc && field.doc.name === frappe._from_link.docname) { | |||
frappe._from_link.set_value(newdoc.name); | |||
frappe.model.with_doctype(newdoc.doctype, () => { | |||
let meta = frappe.get_meta(newdoc.doctype); | |||
// set value | |||
if (doc && doc.parentfield) { | |||
//update values for child table | |||
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) { | |||
if (field.doc && field.doc.name === frappe._from_link.docname) { | |||
if (meta.title_field && meta.show_title_field_in_link) { | |||
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]); | |||
} | |||
frappe._from_link.set_value(newdoc.name); | |||
} | |||
}); | |||
} else { | |||
if (meta.title_field && meta.show_title_field_in_link) { | |||
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]); | |||
} | |||
}); | |||
} else { | |||
frappe._from_link.set_value(newdoc.name); | |||
} | |||
frappe._from_link.set_value(newdoc.name); | |||
} | |||
// refresh field | |||
frappe._from_link.refresh(); | |||
// refresh field | |||
frappe._from_link.refresh(); | |||
// if from form, switch | |||
if (frappe._from_link.frm) { | |||
frappe.set_route("Form", | |||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname) | |||
.then(() => { | |||
frappe.utils.scroll_to(frappe._from_link_scrollY); | |||
}); | |||
} | |||
// if from form, switch | |||
if (frappe._from_link.frm) { | |||
frappe.set_route("Form", | |||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname) | |||
.then(() => { | |||
frappe.utils.scroll_to(frappe._from_link_scrollY); | |||
}); | |||
} | |||
frappe._from_link = null; | |||
frappe._from_link = null; | |||
}); | |||
} | |||
} |
@@ -260,6 +260,14 @@ frappe.request.call = function(opts) { | |||
$.extend(frappe._messages, data.__messages); | |||
} | |||
// sync link titles | |||
if (data._link_titles) { | |||
if (!frappe._link_titles) { | |||
frappe._link_titles = {}; | |||
} | |||
$.extend(frappe._link_titles, data._link_titles); | |||
} | |||
// callbacks | |||
var status_code_handler = statusCode[xhr.statusCode().status]; | |||
if (status_code_handler) { | |||
@@ -314,6 +314,10 @@ frappe.ui.Filter = class { | |||
return this.utils.get_selected_value(this.field, this.get_condition()); | |||
} | |||
get_selected_label() { | |||
return this.utils.get_selected_label(this.field); | |||
} | |||
get_condition() { | |||
return this.filter_edit_area.find('.condition').val(); | |||
} | |||
@@ -361,7 +365,7 @@ frappe.ui.Filter = class { | |||
get_filter_button_text() { | |||
let value = this.utils.get_formatted_value( | |||
this.field, | |||
this.get_selected_value() | |||
this.get_selected_label() || this.get_selected_value() | |||
); | |||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__( | |||
value | |||
@@ -449,6 +453,12 @@ frappe.ui.filter_utils = { | |||
return val; | |||
}, | |||
get_selected_label(field) { | |||
if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) { | |||
return field.get_label_value(); | |||
} | |||
}, | |||
get_default_condition(df) { | |||
if (df.fieldtype == 'Data') { | |||
return 'like'; | |||
@@ -1416,5 +1416,42 @@ Object.assign(frappe.utils, { | |||
arr.push(i); | |||
} | |||
return arr; | |||
}, | |||
get_link_title(doctype, name) { | |||
if (!doctype || !name || !frappe._link_titles) { | |||
return; | |||
} | |||
return frappe._link_titles[doctype + "::" + name]; | |||
}, | |||
add_link_title(doctype, name, value) { | |||
if (!doctype || !name) { | |||
return; | |||
} | |||
if (!frappe._link_titles) { | |||
// for link titles | |||
frappe._link_titles = {}; | |||
} | |||
frappe._link_titles[doctype + "::" + name] = value; | |||
}, | |||
fetch_link_title(doctype, name) { | |||
try { | |||
return frappe.xcall("frappe.desk.search.get_link_title", { | |||
"doctype": doctype, | |||
"docname": name | |||
}).then(title => { | |||
frappe.utils.add_link_title(doctype, name, title); | |||
return title; | |||
}); | |||
} catch (error) { | |||
console.log('Error while fetching link title.'); // eslint-disable-line | |||
console.log(error); // eslint-disable-line | |||
return Promise.resolve(name); | |||
} | |||
} | |||
}); |
@@ -419,3 +419,96 @@ class TestXlsxUtils(unittest.TestCase): | |||
val = handle_html("<p>html data ></p>") | |||
self.assertIn("html data >", val) | |||
self.assertEqual("abc", handle_html("abc")) | |||
class TestLinkTitle(unittest.TestCase): | |||
def test_link_title_doctypes_in_boot_info(self): | |||
""" | |||
Test that doctypes are added to link_title_map in boot_info | |||
""" | |||
custom_doctype = frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [ | |||
{ | |||
"label": "Test Field", | |||
"fieldname": "test_title_field", | |||
"fieldtype": "Data", | |||
} | |||
], | |||
"show_title_field_in_link": 1, | |||
"title_field": "test_title_field", | |||
"permissions": [{"role": "System Manager", "read": 1}], | |||
"name": "Test Custom Doctype for Link Title", | |||
} | |||
) | |||
custom_doctype.insert() | |||
prop_setter = frappe.get_doc( | |||
{ | |||
"doctype": "Property Setter", | |||
"doc_type": "User", | |||
"property": "show_title_field_in_link", | |||
"property_type": "Check", | |||
"doctype_or_field": "DocType", | |||
"value": "1", | |||
} | |||
).insert() | |||
from frappe.boot import get_link_title_doctypes | |||
link_title_doctypes = get_link_title_doctypes() | |||
self.assertTrue("User" in link_title_doctypes) | |||
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes) | |||
prop_setter.delete() | |||
custom_doctype.delete() | |||
def test_link_titles_on_getdoc(self): | |||
""" | |||
Test that link titles are added to the doctype on getdoc | |||
""" | |||
prop_setter = frappe.get_doc( | |||
{ | |||
"doctype": "Property Setter", | |||
"doc_type": "User", | |||
"property": "show_title_field_in_link", | |||
"property_type": "Check", | |||
"doctype_or_field": "DocType", | |||
"value": "1", | |||
} | |||
).insert() | |||
user = frappe.get_doc( | |||
{ | |||
"doctype": "User", | |||
"user_type": "Website User", | |||
"email": "test_user_for_link_title@example.com", | |||
"send_welcome_email": 0, | |||
"first_name": "Test User for Link Title", | |||
} | |||
).insert(ignore_permissions=True) | |||
todo = frappe.get_doc( | |||
{ | |||
"doctype": "ToDo", | |||
"description": "test-link-title-on-getdoc", | |||
"allocated_to": user.name, | |||
} | |||
).insert() | |||
from frappe.desk.form.load import getdoc | |||
getdoc("ToDo", todo.name) | |||
link_titles = frappe.local.response["_link_titles"] | |||
self.assertTrue(f"{user.doctype}::{user.name}" in link_titles) | |||
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name) | |||
todo.delete() | |||
user.delete() | |||
prop_setter.delete() | |||
@@ -88,9 +88,14 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form | |||
return frappe.utils.markdown(value) | |||
elif df.get("fieldtype") == "Table MultiSelect": | |||
values = [] | |||
meta = frappe.get_meta(df.options) | |||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0] | |||
values = [v.get(link_field.fieldname, 'asdf') for v in value] | |||
for v in value: | |||
v.update({'__link_titles': doc.get('__link_titles')}) | |||
formatted_value = frappe.format_value(v.get(link_field.fieldname, ''), link_field, v) | |||
values.append(formatted_value) | |||
return ', '.join(values) | |||
elif df.get("fieldtype") == "Duration": | |||
@@ -100,4 +105,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form | |||
elif df.get("fieldtype") == "Text Editor": | |||
return "<div class='ql-snow'>{}</div>".format(value) | |||
elif df.get("fieldtype") in ["Link", "Dynamic Link"]: | |||
if not doc or not doc.get("__link_titles") or not df.options: | |||
return value | |||
doctype = df.options | |||
if df.get("fieldtype") == "Dynamic Link": | |||
if not df.parent: | |||
return value | |||
meta = frappe.get_meta(df.parent) | |||
_field = meta.get_field(df.options) | |||
doctype = _field.options | |||
return doc.__link_titles.get("{0}::{1}".format(doctype, value), value) | |||
return value |
@@ -169,6 +169,48 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None, | |||
return html | |||
def set_link_titles(doc): | |||
# Adds name with title of link field doctype to __link_titles | |||
if not doc.get("__link_titles"): | |||
setattr(doc, "__link_titles", {}) | |||
meta = frappe.get_meta(doc.doctype) | |||
set_title_values_for_link_and_dynamic_link_fields(meta, doc) | |||
set_title_values_for_table_and_multiselect_fields(meta, doc) | |||
def set_title_values_for_link_and_dynamic_link_fields(meta, doc, parent_doc=None): | |||
if parent_doc and not parent_doc.get("__link_titles"): | |||
setattr(parent_doc, "__link_titles", {}) | |||
elif doc and not doc.get("__link_titles"): | |||
setattr(doc, "__link_titles", {}) | |||
for field in meta.get_link_fields() + meta.get_dynamic_link_fields(): | |||
if not doc.get(field.fieldname): | |||
continue | |||
# If link field, then get doctype from options | |||
# If dynamic link field, then get doctype from dependent field | |||
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options) | |||
meta = frappe.get_meta(doctype) | |||
if not meta or not (meta.title_field and meta.show_title_field_in_link): | |||
continue | |||
link_title = frappe.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field) | |||
if parent_doc: | |||
parent_doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title | |||
elif doc: | |||
doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title | |||
def set_title_values_for_table_and_multiselect_fields(meta, doc): | |||
for field in meta.get_table_fields(): | |||
if not doc.get(field.fieldname): | |||
continue | |||
_meta = frappe.get_meta(field.options) | |||
for value in doc.get(field.fieldname): | |||
set_title_values_for_link_and_dynamic_link_fields(_meta, value, doc) | |||
def convert_markdown(doc, meta): | |||
'''Convert text field values to markdown if necessary''' | |||
for field in meta.fields: | |||
@@ -190,6 +232,7 @@ def get_html_and_style(doc, name=None, print_format=None, meta=None, | |||
doc = frappe.get_doc(json.loads(doc)) | |||
print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype)) | |||
set_link_titles(doc) | |||
try: | |||
html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta, | |||