diff --git a/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js new file mode 100644 index 0000000000..3bf3e829f9 --- /dev/null +++ b/cypress/integration/control_autocomplete.js @@ -0,0 +1,57 @@ +context('Control Autocomplete', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + function get_dialog_with_autocomplete(options) { + cy.visit('/app/website'); + return cy.dialog({ + title: 'Autocomplete', + fields: [ + { + 'label': 'Select an option', + 'fieldname': 'autocomplete', + 'fieldtype': 'Autocomplete', + 'options': options || ['Option 1', 'Option 2', 'Option 3'], + } + ] + }); + } + + it('should set the valid value', () => { + get_dialog_with_autocomplete().as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.wait(1000); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('Option 2'); + dialog.clear(); + }); + }); + + it('should set the valid value with different label', () => { + const options_with_label = [ + { label: "Option 1", value: "option_1" }, + { label: "Option 2", value: "option_2" } + ]; + get_dialog_with_autocomplete(options_with_label).as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('option_2'); + dialog.clear(); + }); + }); + +}); diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 6eb8cf347f..3267429298 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -99,7 +99,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -547,7 +547,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-27 21:22:20.529072", + "modified": "2022-02-14 11:56:19.812863", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index bf63afa5c5..7d434ff166 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -330,7 +330,8 @@ result = [ } ] - result = add_total_row(result, columns, meta=None, report_settings=report_settings) + result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'], + parent_field=report_settings['parent_field']) self.assertEqual(result[-1][0], "Total") self.assertEqual(result[-1][1], 200) self.assertEqual(result[-1][2], 150.50) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index e51dfda14b..f09829a688 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -122,7 +122,7 @@ "label": "Field Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", "reqd": 1 }, { @@ -431,7 +431,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-01-27 21:47:01.065556", + "modified": "2022-02-14 15:42:21.885999", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 5906cd3bcf..1cc4c9f623 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -85,7 +85,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -450,7 +450,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-08 19:38:16.111199", + "modified": "2022-02-25 16:01:12.616736", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", @@ -460,4 +460,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} +} \ No newline at end of file diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index de28dad900..b5971e236e 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,7 +52,8 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index a3266242a5..b0793fcbf0 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -62,7 +62,8 @@ class PostgresDatabase(Database): 'Barcode': ('text', ''), 'Geolocation': ('text', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 4d35ebf5e8..b5dfacb1d6 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -11,8 +11,10 @@ from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe.desk.form.document_follow import is_document_followed from frappe import _ +from frappe import _dict from urllib.parse import quote + @frappe.whitelist() def getdoc(doctype, name, user=None): """ @@ -50,8 +52,11 @@ def getdoc(doctype, name, user=None): doc.add_seen() set_link_titles(doc) + if frappe.response.docs is None: + frappe.response = _dict({"docs": []}) frappe.response.docs.append(doc) + @frappe.whitelist() def getdoctype(doctype, with_parent=False, cached_timestamp=None): """load doctype""" diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 74101a6e1f..0c32e886f4 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -392,7 +392,7 @@ def make_records(records, debug=False): doc.flags.ignore_mandatory = True try: - doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + doc.insert(ignore_permissions=True) frappe.db.commit() except frappe.DuplicateEntryError as e: diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9ed956e986..a0b0aec255 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -73,7 +73,7 @@ def get_report_result(report, filters): return res @frappe.read_only() -def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None): +def generate_report_result(report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None): user = user or frappe.session.user filters = filters or [] @@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None, result = get_filtered_data(report.ref_doctype, columns, result, user) if cint(report.add_total_row) and result and not skip_total_row: - result = add_total_row(result, columns, report_settings=report_settings) + result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) return { "result": result, @@ -210,7 +210,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, is_tree=False, parent_field=None): report = get_report_doc(report_name) if not user: user = frappe.session.user @@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user, custom_columns, report_settings) + result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field) result["add_total_row"] = report.add_total_row and not result.get( "skip_total_row", False @@ -435,18 +435,9 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi return result, column_widths -def add_total_row(result, columns, meta=None, report_settings=None): +def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): total_row = [""] * len(columns) has_percent = [] - is_tree = False - parent_field = '' - - if report_settings: - if isinstance(report_settings, (str,)): - report_settings = json.loads(report_settings) - - is_tree = report_settings.get('tree') - parent_field = report_settings.get('parent_field') for i, col in enumerate(columns): fieldtype, options, fieldname = None, None, None diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index be9496c85b..ab792d90e5 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -35,7 +35,8 @@ data_fieldtypes = ( 'Barcode', 'Geolocation', 'Duration', - 'Icon' + 'Icon', + 'Autocomplete', ) attachment_fieldtypes = ( diff --git a/frappe/model/document.py b/frappe/model/document.py index cb36c18b47..37e70e8126 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1154,7 +1154,7 @@ class Document(BaseDocument): for f in hooks: add_to_return_value(self, f(self, method, *args, **kwargs)) - return self._return_value + return self.__dict__.pop("_return_value", None) return runner diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 1bc0ffeb8a..4e66ed6642 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -11,7 +11,26 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui set_options() { if (this.df.options) { let options = this.df.options || []; - this._data = this.parse_options(options); + this.set_data(options); + } + } + + format_for_input(value) { + if (value == null) { + return ""; + } else if (this._data && this._data.length) { + const item = this._data.find(i => i.value == value); + return item ? item.label : value; + } else { + return value; + } + } + + get_input_value() { + if (this.$input) { + const label = this.$input.val(); + const item = this._data?.find(i => i.label == label); + return item ? item.value : label; } } @@ -23,7 +42,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui autoFirst: true, list: this.get_data(), data: function(item) { - if (!(item instanceof Object)) { + if (typeof item !== 'object') { var d = { value: item }; item = d; } @@ -65,6 +84,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui }; } + init_option_cache() { + if (!this.$input.cache) { + this.$input.cache = {}; + } + if (!this.$input.cache[this.doctype]) { + this.$input.cache[this.doctype] = {}; + } + if (!this.$input.cache[this.doctype][this.df.fieldname]) { + this.$input.cache[this.doctype][this.df.fieldname] = {}; + } + } + setup_awesomplete() { this.awesomplete = new Awesomplete( this.input, @@ -75,12 +106,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui .find('.awesomplete ul') .css('min-width', '100%'); - this.$input.on( - 'input', - frappe.utils.debounce(() => { + this.init_option_cache(); + + this.$input.on('input', frappe.utils.debounce((e) => { + const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; + if (cached_options && cached_options.length) { + this.set_data(cached_options); + } else if (this.get_query || this.df.get_query) { + this.execute_query_if_exists(e.target.value); + } else { this.awesomplete.list = this.get_data(); - }, 500) - ); + } + }, 500)); this.$input.on('focus', () => { if (!this.$input.val()) { @@ -89,6 +126,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } }); + this.$input.on("blur", () => { + if(this.selected) { + this.selected = false; + return; + } + var value = this.get_input_value(); + if(value!==this.last_value) { + this.parse_validate_and_set_in_model(value); + } + }); + this.$input.on("awesomplete-open", () => { this.autocomplete_open = true; }); @@ -127,6 +175,75 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui return options; } + execute_query_if_exists(term) { + const args = { txt: term }; + let get_query = this.get_query || this.df.get_query; + + if (!get_query) { + return; + } + + let set_nulls = function(obj) { + $.each(obj, function(key, value) { + if (value !== undefined) { + obj[key] = value; + } + }); + return obj; + }; + + let process_query_object = function(obj) { + if (obj.query) { + args.query = obj.query; + } + + if (obj.params) { + set_nulls(obj.params); + Object.assign(args, obj.params); + } + + // turn off value translation + if (obj.translate_values !== undefined) { + this.translate_values = obj.translate_values; + } + }; + + if ($.isPlainObject(get_query)) { + process_query_object(get_query); + } else if (typeof get_query === "string") { + args.query = get_query; + } else { + // get_query by function + var q = get_query( + (this.frm && this.frm.doc) || this.doc, + this.doctype, + this.docname + ); + + if (typeof q === "string") { + // returns a string + args.query = q; + } else if ($.isPlainObject(q)) { + // returns an object + process_query_object(q); + } + } + + if (args.query) { + frappe.call({ + method: args.query, + args: args, + callback: ({ message }) => { + if(!this.$input.is(":focus")) { + return; + } + this.$input.cache[this.doctype][this.df.fieldname][term] = message; + this.set_data(message); + } + }) + } + } + get_data() { return this._data || []; } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index c39c4046b4..2b0f996661 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -21,6 +21,9 @@ frappe.form.formatters = { } return value==null ? "" : value; }, + Autocomplete: function(value) { + return __(frappe.form.formatters["Data"](value)); + }, Select: function(value) { return __(frappe.form.formatters["Data"](value)); }, diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index cd8bde1f57..2e9c6f970a 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -183,21 +183,20 @@ export default class GridRow { render_template() { this.set_row_index(); - if(this.row_display) { + if (this.row_display) { this.row_display.remove(); } // row index - if(this.doc) { - if(!this.row_index) { - this.row_index = $('
'+this.row_check_html+'
').appendTo(this.row); - } + if (!this.row_index) { + this.row_index = $(`
${this.row_check_html}
`).appendTo(this.row); + } + + if (this.doc) { this.row_index.find('span').html(this.doc.idx); } - this.row_display = $('
'+ - +'
').appendTo(this.row) + this.row_display = $('
').appendTo(this.row) .html(frappe.render(this.grid.template, { doc: this.doc ? frappe.get_format_helper(this.doc) : null, frm: this.frm, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index c226a3a458..7c12809fcd 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -578,7 +578,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { args: { report_name: this.report_name, filters: filters, - report_settings: this.report_settings + is_tree: this.report_settings.tree, + parent_field: this.report_settings.parent_field }, callback: resolve, always: () => this.page.btn_secondary.prop('disabled', false) diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index bfce93dbcc..ab6bcfe8e8 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -54,7 +54,7 @@ } .form-grid .grid-heading-row .template-row { - margin-left: 20px; + margin-left: 8px; } .form-grid .template-row { @@ -88,6 +88,17 @@ margin-top: 2px; } +.template-row-index { + float: left; + margin-left: 15px; + margin-top: 8px; + margin-right: -20px; + + span { + margin-left: 5px; + } +} + .editable-form .grid-static-col.bold { font-weight: bold; } diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 86843302e9..5e6a9df16f 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -164,6 +164,7 @@ def get_alert_dict(doc): return alert_dict + def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False): doc = frappe._dict(doc) @@ -171,7 +172,7 @@ def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False): ref_name, doc.rule, None if apply_only_once else doc.user) if log_exists: - return + return frappe.get_doc('Energy Point Log', log_exists) new_log = frappe.new_doc('Energy Point Log') new_log.reference_doctype = ref_doctype