diff --git a/cypress.json b/cypress.json index ae0c45c3ae..97ac41bb61 100644 --- a/cypress.json +++ b/cypress.json @@ -2,6 +2,6 @@ "baseUrl": "http://test_site_ui:8000", "projectId": "92odwv", "adminPassword": "admin", - "defaultCommandTimeout": 10000, + "defaultCommandTimeout": 20000, "pageLoadTimeout": 15000 } diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 658a7fe320..0dc7d5b88e 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -1,7 +1,11 @@ context('Control Link', () => { - beforeEach(() => { + before(() => { cy.login(); cy.visit('/desk#workspace/Website'); + }); + + beforeEach(() => { + cy.visit('/desk#workspace/Website'); cy.create_records({ doctype: 'ToDo', description: 'this is a test todo for link' @@ -30,7 +34,7 @@ context('Control Link', () => { cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); cy.wait('@search_link'); - cy.get('@input').type('todo for link'); + cy.get('@input').type('todo for link', { delay: 200 }); 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 }); diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js index 986c5ce342..411ede62fa 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_filters.js @@ -1,7 +1,6 @@ context('Relative Timeframe', () => { beforeEach(() => { cy.login(); - cy.visit('/desk#workspace/Website'); }); before(() => { cy.login(); diff --git a/frappe/boot.py b/frappe/boot.py index 0eb6265942..e615cc49fa 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -106,7 +106,7 @@ def load_desktop_data(bootinfo): from frappe.desk.desktop import get_desk_sidebar_items bootinfo.allowed_modules = get_modules_from_all_apps_for_user() bootinfo.allowed_workspaces = get_desk_sidebar_items(True) - bootinfo.dashboards = frappe.get_list("Dashboard") + bootinfo.dashboards = frappe.get_all("Dashboard") def get_allowed_pages(cache=False): return get_user_pages_or_reports('Page', cache=cache) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 6cb8f8bfd9..ab1863ca0b 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -137,7 +137,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): to_date = datetime.datetime.now() doctype = chart.document_type - unit_function = get_unit_function(doctype, chart.based_on, timegrain) datefield = chart.based_on aggregate_function = get_aggregate_function(chart.chart_type) value_field = chart.value_based_on or '1' @@ -150,23 +149,18 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): data = frappe.db.get_list( doctype, fields = [ - 'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield), - '{} as _unit'.format(unit_function), + '{} as _unit'.format(datefield), '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field), ], filters = filters, - group_by = '_year, _unit', - order_by = '_year asc, _unit asc', + group_by = '_unit', + order_by = '_unit asc', as_list = True, ignore_ifnull = True ) + result = get_result(data, timegrain, from_date, to_date) - # result given as year, unit -> convert it to end of period of that unit - result = convert_to_dates(data, timegrain) - - # add missing data points for periods where there was no result - result = add_missing_values(result, timegrain, timespan, from_date, to_date) chart_config = { "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "datasets": [{ @@ -261,75 +255,22 @@ def get_aggregate_function(chart_type): }[chart_type] -def convert_to_dates(data, timegrain): - """ Converts individual dates within data to the end of period """ - result = [] - for d in data: - if d[2] != 0: - if timegrain == 'Daily': - result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]]) - elif timegrain == 'Weekly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]]) - elif timegrain == 'Monthly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]]) - elif timegrain == 'Quarterly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]]) - elif timegrain == 'Yearly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]]) - result[-1][0] = getdate(result[-1][0]) - - return result - -def get_unit_function(doctype, datefield, timegrain): - unit_function = '' - if timegrain=='Daily': - if frappe.db.db_type == 'mariadb': - unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format( - doctype=doctype, datefield=datefield) - else: - unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format( - doctype=doctype, datefield=datefield) - - else: - unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format( - unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield) - - return unit_function - -def add_missing_values(data, timegrain, timespan, from_date, to_date): - # add missing intervals +def get_result(data, timegrain, from_date, to_date): + start_date = getdate(from_date) + end_date = getdate(to_date) result = [] - if timespan != 'All Time': - first_expected_date = get_period_ending(from_date, timegrain) - # fill out data before the first data point - first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) - while first_data_point_date > first_expected_date: - result.append([first_expected_date, 0.0]) - first_expected_date = get_next_expected_date(first_expected_date, timegrain) + while start_date <= end_date: + next_date = get_next_expected_date(start_date, timegrain) + result.append([next_date, 0.0]) + start_date = next_date - # fill data points and missing points - for i, d in enumerate(data): - result.append(d) - - next_expected_date = get_next_expected_date(d[0], timegrain) - - if i < len(data)-1: - next_date = data[i+1][0] - else: - # already reached at end of data, see if we need any more dates - next_date = getdate(nowdate()) - - # if next data point is earler than the expected date - # need to fill out missing data points - while next_date > next_expected_date: - # fill missing value - result.append([next_expected_date, 0.0]) - next_expected_date = get_next_expected_date(next_expected_date, timegrain) - - # add date for the last period (if missing) - if result and get_period_ending(to_date, timegrain) > result[-1][0]: - result.append([get_period_ending(to_date, timegrain), 0.0]) + data_index = 0 + if data: + for i, d in enumerate(result): + while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: + d[1] += data[data_index][1] + data_index += 1 return result @@ -358,17 +299,12 @@ def get_period_ending(date, timegrain): return getdate(date) def get_week_ending(date): - # fun fact: week ends on the day before 1st Jan of the year. - # for 2019 it is Monday + # week starts on monday + from datetime import timedelta + start = date - timedelta(days = date.weekday()) + end = start + timedelta(days=6) - week_of_the_year = int(date.strftime('%U')) - - if week_of_the_year == 52: - date = add_to_date(date, years=1) - # first day of next week - date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1) - # last day of this week - return add_to_date(date, days=-1) + return end def get_month_ending(date): month_of_the_year = int(date.strftime('%m')) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 4425c4fd45..dfc6edbf58 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -17,10 +17,9 @@ class TestDashboardChart(unittest.TestCase): self.assertEqual(get_period_ending('2019-04-10', 'Daily'), getdate('2019-04-10')) - # fun fact: week ends on the day before 1st Jan of the year. - # for 2019 it is Monday + # week starts on monday self.assertEqual(get_period_ending('2019-04-10', 'Weekly'), - getdate('2019-04-15')) + getdate('2019-04-14')) self.assertEqual(get_period_ending('2019-04-10', 'Monthly'), getdate('2019-04-30')) @@ -133,6 +132,34 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() + def test_weekly_dashboard_chart(self): + insert_test_records() + + if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart') + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Weekly Dashboard Chart', + chart_type = 'Sum', + document_type = 'Communication', + based_on = 'communication_date', + value_based_on = 'rating', + timespan = 'Select Date Range', + time_interval = 'Weekly', + from_date = datetime(2018, 12, 30), + to_date = datetime(2019, 1, 15), + filters_json = '[]', + timeseries = 1 + )).insert() + + result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) + + self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0]) + self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) + + frappe.db.rollback() + def test_group_by_chart_type(self): if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') @@ -155,17 +182,16 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() - def test_dashboard_with_single_doctype(self): - if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'): - frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart') +def insert_test_records(): + create_new_communication(datetime(2019, 1, 10), 100) + create_new_communication(datetime(2019, 1, 6), 200) + create_new_communication(datetime(2019, 1, 8), 300) - chart_doc = frappe.get_doc(dict( - doctype = 'Dashboard Chart', - chart_name = 'Test Single DocType In Dashboard Chart', - chart_type = 'Count', - document_type = 'System Settings', - group_by_based_on = 'Created On', - filters_json = '{}', - )) - - self.assertRaises(frappe.ValidationError, chart_doc.insert) +def create_new_communication(date, rating): + communication = { + 'doctype': 'Communication', + 'subject': 'Test Communication', + 'rating': rating, + 'communication_date': date + } + frappe.get_doc(communication).insert() diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 6c679bf312..72917d0341 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module @frappe.whitelist() -def get_submitted_linked_docs(doctype, name, docs=None): +def get_submitted_linked_docs(doctype, name, docs=None, linked=None): """ Get all nested submitted linked doctype linkinfo @@ -31,12 +31,26 @@ def get_submitted_linked_docs(doctype, name, docs=None): if not docs: docs = [] + if not linked: + linked = {} + linkinfo = get_linked_doctypes(doctype) linked_docs = get_linked_docs(doctype, name, linkinfo) link_count = 0 for link_doctype, link_names in linked_docs.items(): + if link_doctype not in linked: + linked[link_doctype] = [] + for link in link_names: + if link['name'] == name: + continue + + if linked and name in linked[link_doctype]: + continue + + linked[link_doctype].append(link['name']) + docinfo = link.update({"doctype": link_doctype}) validated_doc = validate_linked_doc(docinfo) @@ -47,7 +61,7 @@ def get_submitted_linked_docs(doctype, name, docs=None): if link.name in [doc.get("name") for doc in docs]: continue - links = get_submitted_linked_docs(link_doctype, link.name, docs) + links = get_submitted_linked_docs(link_doctype, link.name, docs, linked) docs.append({ "doctype": link_doctype, "name": link.name, diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 74e841f107..0edfd57d4f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -62,8 +62,16 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) ljust_list(res, 6) if report.custom_columns: + # Original query columns, needed to reorder data as per custom columns + query_columns = columns + # Reordered columns columns = json.loads(report.custom_columns) + + if report.report_type == 'Query Report': + result = reorder_data_for_custom_columns(columns, query_columns, result) + result = add_data_to_custom_columns(columns, result) + if custom_columns: result = add_data_to_custom_columns(custom_columns, result) @@ -208,6 +216,23 @@ def add_data_to_custom_columns(columns, result): return data +def reorder_data_for_custom_columns(custom_columns, columns, result): + reordered_result = [] + columns = [col.split(":")[0] for col in columns] + + for res in result: + r = [] + for col in custom_columns: + try: + idx = columns.index(col.get("label")) + r.append(res[idx]) + except ValueError: + pass + + reordered_result.append(r) + + return reordered_result + def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None diff --git a/frappe/model/meta.py b/frappe/model/meta.py index c8fd1a2ac2..0c5ec75597 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -437,7 +437,7 @@ class Meta(Document): if not self.custom: for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): - data = frappe.get_attr(hook)(data=data) + data = frappe._dict(frappe.get_attr(hook)(data=data)) return data diff --git a/frappe/patches.txt b/frappe/patches.txt index 8ab9418e6c..d164258c42 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -278,6 +278,8 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view frappe.patches.v13_0.migrate_translation_column_data frappe.patches.v13_0.set_read_times frappe.patches.v13_0.remove_web_view +frappe.patches.v13_0.set_unique_for_page_view frappe.patches.v13_0.remove_tailwind_from_page_builder frappe.patches.v13_0.rename_onboarding frappe.patches.v13_0.email_unsubscribe +execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1) diff --git a/frappe/patches/v13_0/set_unique_for_page_view.py b/frappe/patches/v13_0/set_unique_for_page_view.py new file mode 100644 index 0000000000..2a084e52e3 --- /dev/null +++ b/frappe/patches/v13_0/set_unique_for_page_view.py @@ -0,0 +1,6 @@ +import frappe + +def execute(): + frappe.reload_doc('website', 'doctype', 'web_page_view', force=True) + site_url = frappe.utils.get_site_url(frappe.local.site) + frappe.db.sql("""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url)) diff --git a/frappe/public/build.json b/frappe/public/build.json index 30cb2adf87..997a3092ad 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -250,6 +250,8 @@ "public/less/form_grid.less" ], "js/form.min.js": [ + "public/js/frappe/form/templates/address_list.html", + "public/js/frappe/form/templates/contact_list.html", "public/js/frappe/form/templates/print_layout.html", "public/js/frappe/form/templates/users_in_sidebar.html", "public/js/frappe/form/templates/set_sharing.html", diff --git a/frappe/public/js/frappe/form/controls/multiselect_pills.js b/frappe/public/js/frappe/form/controls/multiselect_pills.js index 8796c95eaa..6190204357 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_pills.js +++ b/frappe/public/js/frappe/form/controls/multiselect_pills.js @@ -129,7 +129,8 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte get_data() { let data; if(this.df.get_data) { - data = this.df.get_data(); + let txt = this.$input.val(); + data = this.df.get_data(txt); if (data && data.then) { data.then((r) => { this.set_data(r); diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index cdd385a6ea..41b87e0207 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -1,110 +1,62 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.form.MultiSelectDialog = Class.extend({ - init: function(opts) { - /* Options: doctype, target, setters, get_query, action */ - $.extend(this, opts); - +frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { + constructor(opts) { + /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ + Object.assign(this, opts); var me = this; - if(this.doctype!="[Select]") { - frappe.model.with_doctype(this.doctype, function(r) { + if (this.doctype != "[Select]") { + frappe.model.with_doctype(this.doctype, function () { me.make(); }); } else { this.make(); } - }, - make: function() { - let me = this; + } + make() { + let me = this; this.page_length = 20; this.start = 0; + let fields = this.get_primary_filters(); - let fields = [ - { - fieldtype: "Data", - label: __("Search Term"), - fieldname: "search_term" - }, - { - fieldtype: "Column Break" - } - ]; - let count = 0; - if(!this.date_field) { - this.date_field = "transaction_date"; - } - - // setters can be defined as a dict or a list of fields - // setters define the additional filters that get applied - // for selection - - // CASE 1: DocType name and fieldname is the same, example "customer" and "customer" - // setters define the filters applied in the modal - // if the fieldnames and doctypes are consistently named, - // pass a dict with the setter key and value, for example - // {customer: [customer_name]} - - // CASE 2: if the fieldname of the target is different, - // then pass a list of fields with appropriate fieldname - - if($.isArray(this.setters)) { - for (let df of this.setters) { - fields.push(df, {fieldtype: "Column Break"}); - } - } else { - Object.keys(this.setters).forEach(function(setter) { - fields.push({ - fieldtype: me.target.fields_dict[setter].df.fieldtype, - label: me.target.fields_dict[setter].df.label, - fieldname: setter, - options: me.target.fields_dict[setter].df.options, - default: me.setters[setter] - }); - if (count++ < Object.keys(me.setters).length) { - fields.push({fieldtype: "Column Break"}); - } - }); - } - + // Make results area fields = fields.concat([ - { - "fieldname":"date_range", - "label": __("Date Range"), - "fieldtype": "DateRange", - }, - { fieldtype: "Section Break" }, { fieldtype: "HTML", fieldname: "results_area" }, - { fieldtype: "Button", fieldname: "more_btn", label: __("More"), - click: function(){ - me.start += 20; - frappe.flags.auto_scroll = true; - me.get_results(); + { + fieldtype: "Button", fieldname: "more_btn", label: __("More"), + click: () => { + this.start += 20; + this.get_results(); } } ]); - let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' - : this.doctype.slice(0, -1) + 'ies'; + // Custom Data Fields + if (this.data_fields) { + fields.push({ fieldtype: "Section Break" }); + fields = fields.concat(this.data_fields); + } + + let doctype_plural = this.doctype.plural(); + this.dialog = new frappe.ui.Dialog({ - title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), + title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), fields: fields, - primary_action_label: __("Get Items"), + primary_action_label: this.primary_action_label || __("Get Items"), secondary_action_label: __("Make {0}", [me.doctype]), - primary_action: function() { - me.action(me.get_checked_values(), me.args); + primary_action: function () { + let filters_data = me.get_custom_filters(); + me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data); }, - secondary_action: function(e) { + secondary_action: function (e) { // If user wants to close the modal if (e) { frappe.route_options = {}; - if($.isArray(me.setters)) { + if (Array.isArray(me.setters)) { for (let df of me.setters) { frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; } } else { - Object.keys(me.setters).forEach(function(setter) { + Object.keys(me.setters).forEach(function (setter) { frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; }); } @@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ } }); + if (this.add_filters_group) { + this.make_filter_area(); + } + this.$parent = $(this.dialog.body); this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`
`); @@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ this.bind_events(); this.get_results(); this.dialog.show(); - }, + } - bind_events: function() { + get_primary_filters() { + let fields = []; + + let columns = new Array(3); + + // Hack for three column layout + // To add column break + columns[0] = [ + { + fieldtype: "Data", + label: __("Search"), + fieldname: "search_term" + } + ]; + columns[1] = []; + columns[2] = []; + + Object.keys(this.setters).forEach((setter, index) => { + let df_prop = frappe.meta.docfield_map[this.doctype][setter]; + + // Index + 1 to start filling from index 1 + // Since Search is a standrd field already pushed + columns[(index + 1) % 3].push({ + fieldtype: df_prop.fieldtype, + label: df_prop.label, + fieldname: setter, + options: df_prop.options, + default: this.setters[setter] + }); + }); + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal + if (Object.seal) { + Object.seal(columns); + // now a is a fixed-size array with mutable entries + } + + fields = [ + ...columns[0], + { fieldtype: "Column Break" }, + ...columns[1], + { fieldtype: "Column Break" }, + ...columns[2], + { fieldtype: "Section Break", fieldname: "primary_filters_sb" } + ]; + + if (this.add_filters_group) { + fields.push( + { + fieldtype: 'HTML', + fieldname: 'filter_area', + } + ); + } + + return fields; + } + + make_filter_area() { + this.filter_group = new frappe.ui.FilterGroup({ + parent: this.dialog.get_field('filter_area').$wrapper, + doctype: this.doctype, + on_change: () => { + this.get_results(); + } + }); + } + + get_custom_filters() { + if (this.add_filters_group && this.filter_group) { + return this.filter_group.get_filters().reduce((acc, filter) => { + return Object.assign(acc, { + [filter[1]]: [filter[2], filter[3]] + }); + }, {}); + } else { + return []; + } + } + + bind_events() { let me = this; this.$results.on('click', '.list-item-container', function (e) { @@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ $(this).find(':checkbox').trigger('click'); } }); + this.$results.on('click', '.list-item--head :checkbox', (e) => { this.$results.find('.list-item-container .list-row-check') .prop("checked", ($(e.target).is(':checked'))); }); - this.$parent.find('.input-with-feedback').on('change', (e) => { + this.$parent.find('.input-with-feedback').on('change', () => { frappe.flags.auto_scroll = false; this.get_results(); }); - this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => { - frappe.flags.auto_scroll = false; - this.get_results(); - }); - - this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => { + this.$parent.find('[data-fieldtype="Data"]').on('input', () => { var $this = $(this); clearTimeout($this.data('timeout')); - $this.data('timeout', setTimeout(function() { + $this.data('timeout', setTimeout(function () { frappe.flags.auto_scroll = false; me.empty_list(); me.get_results(); }, 300)); }); - }, + } - get_checked_values: function() { + get_checked_values() { // Return name of checked value. - return this.$results.find('.list-item-container').map(function() { - if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) { + return this.$results.find('.list-item-container').map(function () { + if ($(this).find('.list-row-check:checkbox:checked').length > 0) { return $(this).attr('data-item-name'); } }).get(); - }, + } - get_checked_items: function() { + get_checked_items() { // Return checked items with all the column values. let checked_values = this.get_checked_values(); return this.results.filter(res => checked_values.includes(res.name)); - }, + } - make_list_row: function(result={}) { + make_list_row(result = {}) { var me = this; // Make a head row by default (if result not passed) let head = Object.keys(result).length === 0; @@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ let contents = ``; let columns = ["name"]; - if($.isArray(this.setters)) { - for (let df of this.setters) { - columns.push(df.fieldname); - } - } else { - columns = columns.concat(Object.keys(this.setters)); - } - columns.push("Date"); + columns = columns.concat(Object.keys(this.setters)); - columns.forEach(function(column) { + columns.forEach(function (column) { contents += `+ {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %} + ({%= __(addr_list[i].address_type) %}){% } %} + {% if(addr_list[i].is_primary_address) { %} + ({%= __("Primary") %}){% } %} + {% if(addr_list[i].is_shipping_address) { %} + ({%= __("Shipping") %}){% } %} + + + {%= __("Edit") %} +
+{%= addr_list[i].display %}
+{%= __("No address added yet.") %}
+{% } %} + \ No newline at end of file diff --git a/frappe/public/js/frappe/form/templates/contact_list.html b/frappe/public/js/frappe/form/templates/contact_list.html new file mode 100644 index 0000000000..7e6969163b --- /dev/null +++ b/frappe/public/js/frappe/form/templates/contact_list.html @@ -0,0 +1,54 @@ + +{% for(var i=0, l=contact_list.length; i+ {%= contact_list[i].first_name %} {%= contact_list[i].last_name %} + {% if(contact_list[i].is_primary_contact) { %} + ({%= __("Primary") %}) + {% } %} + {% if(contact_list[i].designation){ %} + – {%= contact_list[i].designation %} + {% } %} + + {%= __("Edit") %} +
+ {% if (contact_list[i].phones || contact_list[i].email_ids) { %} +
+ {% if(contact_list[i].phone) { %}
+ {%= __("Phone") %}: {%= contact_list[i].phone %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].mobile_no) { %}
+ {%= __("Mobile No") %}: {%= contact_list[i].mobile_no %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].phone_nos) { %}
+ {% for(var j=0, k=contact_list[i].phone_nos.length; j
+ {% if(contact_list[i].email_id) { %}
+ {%= __("Email") %}: {%= contact_list[i].email_id %} ({%= __("Primary") %})
+ {% endif %}
+ {% if(contact_list[i].email_ids) { %}
+ {% for(var j=0, k=contact_list[i].email_ids.length; j
+ {% if (contact_list[i].address) { %}
+ {%= __("Address") %}: {%= contact_list[i].address %}
+ {% endif %}
+
{%= __("No contacts added yet.") %}
+{% } %} ++
\ No newline at end of file diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue index 68269ad0f4..53b3c8720b 100644 --- a/frappe/public/js/frappe/recorder/RecorderDetail.vue +++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue @@ -114,8 +114,8 @@ export default { {label: "Time", slug: "time", sortable: true}, ], query: { - sort: "time", - order: "asc", + sort: "duration", + order: "desc", filters: {}, pagination: { limit: 20, diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue index 60795076ec..ac349d7937 100644 --- a/frappe/public/js/frappe/recorder/RequestDetail.vue +++ b/frappe/public/js/frappe/recorder/RequestDetail.vue @@ -79,7 +79,7 @@ -