From d056beb0ef6c736807c1ef2efe50a257c6bbe9ec Mon Sep 17 00:00:00 2001 From: hrwx Date: Tue, 3 Aug 2021 15:55:32 +0530 Subject: [PATCH 01/74] feat: show title links in Link Fields --- cypress/integration/control_link.js | 41 +++++- frappe/boot.py | 7 + frappe/core/doctype/doctype/doctype.json | 9 +- .../customize_form/customize_form.json | 9 +- .../doctype/customize_form/customize_form.py | 3 +- frappe/database/mariadb/framework_mariadb.sql | 1 + .../database/postgres/framework_postgres.sql | 1 + frappe/desk/form/load.py | 54 ++++++- frappe/desk/search.py | 47 ++++++- frappe/public/js/controls.bundle.js | 1 + frappe/public/js/frappe/form/controls/link.js | 133 +++++++++++++++--- .../frappe/form/controls/table_multiselect.js | 7 +- frappe/public/js/frappe/form/formatters.js | 13 +- frappe/public/js/frappe/form/save.js | 55 +++++--- frappe/public/js/frappe/link_title.js | 34 +++++ frappe/public/js/frappe/request.js | 5 + frappe/public/js/frappe/ui/filters/filter.js | 12 +- frappe/www/printview.py | 33 +++++ 18 files changed, 404 insertions(+), 61 deletions(-) create mode 100644 frappe/public/js/frappe/link_title.js diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 8f9257e9c4..e691150925 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -65,16 +65,49 @@ context('Control Link', () => { cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.get('@todos').then(todos => { - cy.get('.frappe-control[data-fieldname=link] input').as('input'); - cy.get('@input').focus(); + cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); cy.wait('@search_link'); - cy.get('@input').type(todos[0]).blur(); + cy.get('@input').type(todos[0]); + 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.wait('@validate_link'); cy.get('@input').focus(); cy.get('.frappe-control[data-fieldname=link] .link-btn') .should('be.visible') .click(); - cy.location('pathname').should('eq', `/app/todo/${todos[0]}`); + cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`); + }); + }); + + it('show title field in link', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.insert_doc("Property Setter", { + property: "show_title_field_in_link", + doc_type: "ToDo", + value: 1, + doctype_or_field: "DocType" + }, true); + cy.route('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'); + }); }); }); }); diff --git a/frappe/boot.py b/frappe/boot.py index 0589e32ac8..8f02b67934 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -87,6 +87,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.doctypes_with_show_link_field_title = doctypes_with_show_link_field_title() return bootinfo @@ -324,3 +325,9 @@ def get_desk_settings(): def get_notification_settings(): return frappe.get_cached_doc('Notification Settings', frappe.session.user) + +def doctypes_with_show_link_field_title(): + dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) + custom_dts = frappe.get_all("Property Setter", {"field_name": "show_title_field_in_link", "value": 1}) + + return [d.name for d in dts + custom_dts if d] \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 6a427f71e1..3024fb32a2 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -45,6 +45,7 @@ "allow_auto_repeat", "view_settings", "title_field", + "show_title_field_in_link", "search_fields", "default_print_format", "sort_field", @@ -554,6 +555,12 @@ "fieldname": "website_search_field", "fieldtype": "Data", "label": "Website Search Field" + }, + { + "default": "0", + "fieldname": "show_title_field_in_link", + "fieldtype": "Check", + "label": "Show Title in Link and Table MultiSelect Field" } ], "icon": "fa fa-bolt", @@ -635,7 +642,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2021-06-17 23:31:44.974199", + "modified": "2021-08-03 13:41:50.319555", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index c2940a92e3..cdcac1582a 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -27,6 +27,7 @@ "autoname", "view_settings_section", "title_field", + "show_title_field_in_link", "image_field", "default_print_format", "column_break_29", @@ -280,6 +281,12 @@ "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name" + }, + { + "default": "0", + "fieldname": "show_title_field_in_link", + "fieldtype": "Check", + "label": "Show Title in Link and Table MultiSelect Field" } ], "hide_toolbar": 1, @@ -288,7 +295,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-21 19:01:06.920663", + "modified": "2021-08-03 13:43:27.938781", "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 8de194fb00..1866f4d368 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -495,7 +495,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 = { diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index f8841e9417..a74ece8478 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -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, PRIMARY KEY (`name`), KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index a4e94aa326..f65a832bc6 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -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, PRIMARY KEY ("name") ) ; diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index a62bfd01d0..994a50f938 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -48,7 +48,7 @@ def getdoc(doctype, name, user=None): raise doc.add_seen() - + set_link_titles(doc) frappe.response.docs.append(doc) @frappe.whitelist(allow_guest=True) @@ -310,3 +310,55 @@ def get_additional_timeline_content(doctype, docname): contents.extend(frappe.get_attr(method)(doctype, docname) or []) return contents + +def set_link_titles(doc): + meta = frappe.get_meta(doc.doctype) + link_titles = {} + link_titles.update(get_title_values_for_link_and_dynamic_link_fields(meta, doc)) + link_titles.update(get_title_values_for_table_and_multiselect_fields(meta, doc)) + + send_link_titles(link_titles) + +def get_title_values_for_link_and_dynamic_link_fields(meta, doc, link_fields=None): + link_titles = {} + + if not link_fields: + 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.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field) + link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title}) + + return link_titles + +def get_title_values_for_table_and_multiselect_fields(meta, doc, table_fields=None): + link_titles = {} + + if not table_fields: + table_fields = meta.get_table_fields() + + for field in table_fields: + if not doc.get(field.fieldname): + continue + + _meta = frappe.get_meta(field.options) + for value in doc.get(field.fieldname): + link_titles.update(get_title_values_for_link_and_dynamic_link_fields(_meta, 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) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index f9b65fc98e..a64d6efec4 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -49,8 +49,11 @@ 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, + is_query=True if query else False) del frappe.response["values"] # this is called by the search box @@ -138,6 +141,11 @@ 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 + 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,10 +213,32 @@ 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 = "NULL as `label`" + + 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, is_query): results = [] for r in res: - out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])} + r = list(r) + if is_query or doctype in (frappe.get_hooks().standard_queries or {}): + out = { + "value": r[0], + "description": ", ".join(unique(cstr(d) for d in r[1:] if d)) + } + else: + out = { + "value": r[0], + "label": r[1], + "description": ", ".join(unique(cstr(d) for d in r[2:] if d)) + } + results.append(out) return results @@ -271,3 +301,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.get_cached_value(doctype, docname, meta.title_field) + + return docname \ No newline at end of file diff --git a/frappe/public/js/controls.bundle.js b/frappe/public/js/controls.bundle.js index 30b5d43905..b08cbb336d 100644 --- a/frappe/public/js/controls.bundle.js +++ b/frappe/public/js/controls.bundle.js @@ -16,3 +16,4 @@ import "air-datepicker/dist/js/i18n/datepicker.sk.js"; import "air-datepicker/dist/js/i18n/datepicker.zh.js"; import "./frappe/ui/capture.js"; import "./frappe/form/controls/control.js"; +import "./frappe/link_title.js"; diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 83f3f8dd70..82481d5303 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -29,12 +29,14 @@ 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)); } if(!me.$input.val()) { + me.reset_value(); + me.reset_fetch_values(me.df, me.docname); me.$input.val("").trigger("input"); } }, 500); @@ -69,6 +71,76 @@ 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; + let doctype = this.get_options(); + this.set_data_value(frappe.get_link_title(doctype, value) || value, value); + } + set_data_value(link_display, value) { + if (!this.$input) { + return; + } + + this.$input.val(__(link_display)); + this.data_value = value; + } + parse_validate_and_set_in_model(value, label, e) { + if (this.parse) value = this.parse(value, label); + if (label) { + this.label = label; + frappe.add_link_title(this.doctype, value, label); + } + + return this.validate_and_set_in_model(value, e); + } + validate_and_set_in_model(value, e) { + var me = this; + if (this.inside_change_event) { + return Promise.resolve(); + } + this.inside_change_event = true; + var set = function(value) { + me.inside_change_event = false; + return frappe.run_serially([ + () => me.set_model_value(value), + () => { + me.set_mandatory && me.set_mandatory(value); + + if (me.df.change || me.df.onchange) { + // onchange event specified in df + frappe.set_link_title(me); + return (me.df.change || me.df.onchange).apply(me, [e]); + } + } + ]); + }; + + value = this.validate(value); + if (value && value.then) { + // got a promise + return value.then((value) => set(value)); + } else { + // all clear + return set(value); + } + } + get_input_value() { + return (this.$input && this.data_value && this.$input.val()) ? this.data_value : ""; + } + get_label_value() { + return this.$input ? this.$input.val() : ""; + } + set_input_label(label) { + this.$input && this.$input.val(__(label)); + } + reset_value() { + if (!this.$input) { + return; + } + this.$input.val(""); + this.data_value = null; + } open_advanced_search() { var doctype = this.get_options(); if(!doctype) return; @@ -98,7 +170,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 +192,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 +313,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, label); } }); @@ -258,14 +337,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,13 +357,14 @@ 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, item.label); }); this.$input.on("awesomplete-selectcomplete", function(e) { - var o = e.originalEvent; - if(o.text.value.indexOf("__link_option") !== -1) { - me.$input.val(""); + let o = e.originalEvent; + if (o.text.value.indexOf("__link_option") !== -1) { + me.reset_value(); + me.reset_fetch_values(me.df, me.docname); } }); } @@ -452,10 +533,13 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat this.docname, value); } validate_link_and_fetch(df, doctype, docname, value) { - if(value) { + let me = this; + + if (value) { return new Promise((resolve) => { - var fetch = ''; - if(this.frm && this.frm.fetch_dict[df.fieldname]) { + let fetch = ''; + + if (this.frm && this.frm.fetch_dict[df.fieldname]) { fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } // if default and no fetch, no need to validate @@ -465,10 +549,14 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch); }); + } else { + me.reset_value(); + me.reset_fetch_values(df, docname); } } - fetch_and_validate_link(resolve, df, doctype, docname, value, fetch) { + let me = this; + frappe.call({ method: 'frappe.desk.form.utils.validate_link', type: "GET", @@ -485,18 +573,27 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat } resolve(r.valid_value); } else { + me.reset_value(); + me.reset_fetch_values(df, docname); resolve(""); } } }); } - set_fetch_values(df, docname, fetch_values) { - var fl = this.frm.fetch_dict[df.fieldname].fields; - for(var i=0; i < fl.length; i++) { + let fl = this.frm.fetch_dict[df.fieldname].fields; + + for (var i=0; i < fl.length; i++) { frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype); } } + reset_fetch_values(df, docname) { + let fields = this.frm && this.frm.fetch_dict && this.frm.fetch_dict[df.fieldname] ? this.frm.fetch_dict[df.fieldname].fields : []; + + fields.forEach(field => { + frappe.model.set_value(df.parent, docname, field, null, df.fieldtype); + }); + } }; if (Awesomplete) { diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index 15dfd9649e..f74e0837ff 100644 --- a/frappe/public/js/frappe/form/controls/table_multiselect.js +++ b/frappe/public/js/frappe/form/controls/table_multiselect.js @@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f [link_field.fieldname]: value }); } + frappe.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 encoded_value = encodeURIComponent(value); + const link_field = this.get_link_field(); + const encoded_value = encodeURIComponent(value.name); + const pill_name = frappe.get_link_title(link_field.options, value[link_field.fieldname]) || value.name; return ` `; diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index b9a838688d..c4f042533d 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -106,12 +106,14 @@ frappe.form.formatters = { Link: function(value, docfield, options, doc) { var doctype = docfield._options || docfield.options; var original_value = value; + let link_title = frappe.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]) { @@ -135,13 +137,14 @@ frappe.form.formatters = { return ` - ${__(options && options.label || value)}`; + data-name="${original_value}" + data-value="${original_value}"> + ${__(options && options.label || link_title || value)}`; } else { - return value; + return link_title || value; } } else { - return value; + return link_title || value; } }, Date: function(value) { diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 65d84e2202..612e449a7e 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -249,31 +249,40 @@ 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); - } - }); - } else { - frappe._from_link.set_value(newdoc.name); - } - - // 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); + 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.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.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]); + } + frappe._from_link.set_value(newdoc.name); + } - frappe._from_link = null; + // 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); + }); + } + + frappe._from_link = null; + }); } } diff --git a/frappe/public/js/frappe/link_title.js b/frappe/public/js/frappe/link_title.js new file mode 100644 index 0000000000..82bc6c149c --- /dev/null +++ b/frappe/public/js/frappe/link_title.js @@ -0,0 +1,34 @@ +// for link titles +frappe._link_titles = {}; + +frappe.get_link_title = function(doctype, name) { + if (!doctype || !name) { + return; + } + + return frappe._link_titles[doctype + "::" + name]; +}; + +frappe.add_link_title = function (doctype, name, value) { + if (!doctype || !name) { + return; + } + + frappe._link_titles[doctype + "::" + name] = value; +}; + +frappe.set_link_title = function(f) { + let doctype = f.get_options(); + let docname = f.get_input_value(); + + if ((!in_list(frappe.boot.doctypes_with_show_link_field_title, doctype)) || (!doctype || !docname) || + (frappe.get_link_title(doctype, docname))) { + return; + } + + frappe.xcall("frappe.desk.search.get_link_title", {"doctype": doctype, "docname": docname}).then((r) => { + if (r && docname !== r) { + f.set_input_label(r); + } + }); +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 12fa9c8e21..9f09b288b6 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -241,6 +241,11 @@ frappe.request.call = function(opts) { $.extend(frappe._messages, data.__messages); } + // sync link titles + if (data._link_titles) { + $.extend(frappe._link_titles, data._link_titles); + } + // callbacks var status_code_handler = statusCode[xhr.statusCode().status]; if (status_code_handler) { diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index b2b2b623e2..d8bc760668 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -313,6 +313,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(); } @@ -360,7 +364,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 @@ -448,6 +452,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'; diff --git a/frappe/www/printview.py b/frappe/www/printview.py index cdf47790eb..268a01eafb 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -170,6 +170,38 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None, return html +def set_link_titles(doc): + # Replaces name with title of link field doctype + + 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): + 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) + setattr(doc, 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) + def convert_markdown(doc, meta): '''Convert text field values to markdown if necessary''' for field in meta.fields: @@ -191,6 +223,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, From bee72ee6ff3ad906a5ac83f5e4ba67d4cc153b34 Mon Sep 17 00:00:00 2001 From: hrwx Date: Tue, 3 Aug 2021 17:15:29 +0530 Subject: [PATCH 02/74] fix: Null query error --- frappe/desk/search.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index a64d6efec4..6d259e3a3d 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -144,7 +144,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, title_field_query = get_title_field_query(meta) # Insert title field query after name - formatted_fields.insert(1, title_field_query) + 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( @@ -216,7 +217,7 @@ def get_std_fields_list(meta, key): 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 = "NULL as `label`" + field = None if title_field and show_title_field_in_link: field = "`tab{0}`.{1} as `label`".format(meta.name, title_field) @@ -224,10 +225,13 @@ def get_title_field_query(meta): return field def build_for_autosuggest(res, doctype, is_query): + meta = frappe.get_meta(doctype) + title_field = get_title_field_query(meta) + _from = 2 if title_field else 1 results = [] for r in res: r = list(r) - if is_query or doctype in (frappe.get_hooks().standard_queries or {}): + if not (meta.title_field and meta.show_title_field_in_link) or doctype in (frappe.get_hooks().standard_queries or {}): out = { "value": r[0], "description": ", ".join(unique(cstr(d) for d in r[1:] if d)) @@ -235,8 +239,8 @@ def build_for_autosuggest(res, doctype, is_query): else: out = { "value": r[0], - "label": r[1], - "description": ", ".join(unique(cstr(d) for d in r[2:] if d)) + "label": r[1] if title_field else None, + "description": ", ".join(unique(cstr(d) for d in r[_from:] if d)) } results.append(out) From 1b3d202eb6e5a10d51500f82b0eabfd7f395260e Mon Sep 17 00:00:00 2001 From: hrwx Date: Fri, 6 Aug 2021 11:31:08 +0530 Subject: [PATCH 03/74] fix: do not modify doc for print formats --- frappe/utils/formatters.py | 15 +++++++++++++++ frappe/www/printview.py | 18 ++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/frappe/utils/formatters.py b/frappe/utils/formatters.py index 9efccc15f0..2ef3ab4fce 100644 --- a/frappe/utils/formatters.py +++ b/frappe/utils/formatters.py @@ -95,4 +95,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False): elif df.get("fieldtype") == "Text Editor": return "
{}
".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 diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 268a01eafb..77fbd0b8e4 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -171,13 +171,20 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None, return html def set_link_titles(doc): - # Replaces name with title of link field doctype + # 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): +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 @@ -191,7 +198,10 @@ def set_title_values_for_link_and_dynamic_link_fields(meta, doc): continue link_title = frappe.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field) - setattr(doc, field.fieldname, link_title) + 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(): @@ -200,7 +210,7 @@ def set_title_values_for_table_and_multiselect_fields(meta, doc): _meta = frappe.get_meta(field.options) for value in doc.get(field.fieldname): - set_title_values_for_link_and_dynamic_link_fields(_meta, value) + 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''' From 0adff72877bac3b9ce400454e65bd9bb9f965d1a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 15 Dec 2021 20:03:56 +0530 Subject: [PATCH 04/74] fix: link title for link fields --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index a3c876fae2..a884399360 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -89,7 +89,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat if (this.parse) value = this.parse(value, label); if (label) { this.label = label; - frappe.add_link_title(this.doctype, value, label); + frappe.add_link_title(this.df.options, value, label); } return this.validate_and_set_in_model(value, e); From d83bcc83542c32a759d1c4df1c067f83f15f548b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 15 Dec 2021 20:14:43 +0530 Subject: [PATCH 05/74] fix: title in table multiselect pills --- frappe/public/js/frappe/form/controls/table_multiselect.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index f74e0837ff..6e26a15249 100644 --- a/frappe/public/js/frappe/form/controls/table_multiselect.js +++ b/frappe/public/js/frappe/form/controls/table_multiselect.js @@ -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) { @@ -128,8 +128,8 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f } get_pill_html(value) { const link_field = this.get_link_field(); - const encoded_value = encodeURIComponent(value.name); - const pill_name = frappe.get_link_title(link_field.options, value[link_field.fieldname]) || value.name; + const encoded_value = encodeURIComponent(value); + const pill_name = frappe.get_link_title(link_field.options, value) || value; return ` `; } + 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(); From 8a264d05cacf8501dab393f009aa899190e5f7d2 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 15 Feb 2022 13:02:20 +0530 Subject: [PATCH 71/74] fix: remove `ignore_in_getter`; cleaner implementation --- frappe/model/base_document.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f6b7d05079..d55ed6cbd2 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -73,8 +73,7 @@ def get_controller(doctype): return site_controllers[doctype] class BaseDocument(object): - ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") - ignore_in_setter = ("doctype",) + ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") def __init__(self, d): if d.get("doctype"): @@ -148,10 +147,11 @@ class BaseDocument(object): else: value = self.__dict__.get(key, default) - if value is None and key not in self.ignore_in_getter \ - and key in (d.fieldname for d in self.meta.get_table_fields()): - self.set(key, []) - value = self.__dict__.get(key) + if value is None and key in ( + d.fieldname for d in self.meta.get_table_fields() + ): + value = [] + self.set(key, value) return value else: From 7ce9f1eaa1acd8c9d7ef618c06dc91182e8e5d93 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Tue, 15 Feb 2022 13:06:55 +0530 Subject: [PATCH 72/74] fix: limit without filter --- frappe/model/base_document.py | 3 +++ frappe/tests/test_document.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index ea4264212b..a386313dc2 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -149,6 +149,9 @@ class BaseDocument(object): self.set(key, []) value = self.__dict__.get(key) + if limit and isinstance(value, (list, tuple)) and len(value) > limit: + value = value[:limit] + return value else: return self.__dict__ diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index a0c44c5c72..71af7e996f 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -264,7 +264,10 @@ class TestDocument(unittest.TestCase): def test_limit_for_get(self): doc = frappe.get_doc("DocType", "DocType") - # assuming DocType has more that 3 Data fields + # assuming DocType has more than 3 Data fields + self.assertEquals(len(doc.get("fields", limit=3)), 3) + + # limit with filters self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) def test_virtual_fields(self): From 81f4e2b00910ee47e8abb6e7b7534e51d2064baa Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 15 Feb 2022 13:47:20 +0530 Subject: [PATCH 73/74] test: UI test for list/report view paging --- cypress/integration/report_view.js | 56 +++++++++++++++++++++++------- frappe/tests/ui_test_helpers.py | 6 ++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 0253e8fd43..5a0b13a3a0 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -11,30 +11,60 @@ context('Report View', () => { 'title': 'Doc 1', 'description': 'Random Text', 'enabled': 0, - // submit document - 'docstatus': 1 - }, true).as('doc'); + 'docstatus': 1 // submit document + }, true); + return cy.window().its('frappe').then(frappe => { + return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records"); + }); }); + it('Field with enabled allow_on_submit should be editable.', () => { cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); + // check status column added from docstatus cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted'); let cell = cy.get('.dt-row-0 > .dt-cell--col-4'); + // select the cell cell.dblclick(); cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true }); + cy.wait('@value-update'); - cy.get('@doc').then(doc => { - cy.call('frappe.client.get_value', { - doctype: doc.doctype, - filters: { - name: doc.name, - }, - fieldname: 'enabled' - }).then(r => { - expect(r.message.enabled).to.equals(1); - }); + + cy.call('frappe.client.get_value', { + doctype: doctype_name, + filters: { + title: 'Doc 1', + }, + fieldname: 'enabled' + }).then(r => { + expect(r.message.enabled).to.equals(1); }); }); + + it('test load more with count selection buttons', () => { + cy.visit('/app/contact/view/report'); + + cy.clear_filters(); + cy.get('.list-paging-area .list-count').should('contain.text', '20 of'); + cy.get('.list-paging-area .btn-more').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '40 of'); + cy.get('.list-paging-area .btn-more').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '60 of'); + + cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click(); + + cy.get('.list-paging-area .list-count').should('contain.text', '100 of'); + cy.get('.list-paging-area .btn-more').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '200 of'); + cy.get('.list-paging-area .btn-more').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); + + cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); + + cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); + cy.get('.list-paging-area .btn-more').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '1000 of'); + }); }); \ No newline at end of file diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index b299df522c..079dcc64ef 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -134,6 +134,12 @@ def create_contact_records(): insert_contact('Test Form Contact 2', '54321') insert_contact('Test Form Contact 3', '12345') +@frappe.whitelist() +def create_multiple_contact_records(): + if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}): + return + for index in range(1001): + insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1)) def insert_contact(first_name, phone_number): doc = frappe.get_doc({ From 8ba50a8b142dcc036ef2f86aa3d641df656e2c21 Mon Sep 17 00:00:00 2001 From: Summayya Hashmani <58825865+sumaiya2908@users.noreply.github.com> Date: Tue, 15 Feb 2022 19:30:08 +0530 Subject: [PATCH 74/74] feat: add button in dashboard list redirecting to dashboard view. (#15695) Co-authored-by: Summayya Co-authored-by: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> --- frappe/desk/doctype/dashboard/dashboard_list.js | 16 ++++++++++++++++ frappe/public/icons/timeless/symbol-defs.svg | 7 +++++++ 2 files changed, 23 insertions(+) create mode 100644 frappe/desk/doctype/dashboard/dashboard_list.js diff --git a/frappe/desk/doctype/dashboard/dashboard_list.js b/frappe/desk/doctype/dashboard/dashboard_list.js new file mode 100644 index 0000000000..d60a324048 --- /dev/null +++ b/frappe/desk/doctype/dashboard/dashboard_list.js @@ -0,0 +1,16 @@ +frappe.listview_settings['Dashboard'] = { + button: { + show(doc) { + return doc.name; + }, + get_label() { + return frappe.utils.icon("dashboard-list", "sm"); + }, + get_description(doc) { + return __('View {0}', [`${doc.name}`]); + }, + action(doc) { + frappe.set_route('dashboard-view', doc.name); + } + }, +}; \ No newline at end of file diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index f2977e3016..bf4e02a7af 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -814,6 +814,13 @@ + + + + + + +