diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 0000000000..b0e11e0e6d --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,13 @@ +pull_request_rules: + - name: Automatic merge on CI success and review + conditions: + - status-success=Codacy/PR Quality Review + - status-success=Semantic Pull Request + - status-success=continuous-integration/travis-ci/pr + - status-success=security/snyk - package.json (frappe) + - status-success=security/snyk - requirements.txt (frappe) + - label!=don't-merge + - "#approved-reviews-by>=1" + actions: + merge: + method: merge diff --git a/cypress.json b/cypress.json index bd93405273..7d853271b9 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,5 @@ { "baseUrl": "http://test_site_ui:8000", - "projectId": "92odwv" + "projectId": "92odwv", + "adminPassword": "admin" } diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 18bfde520c..eafb5b338e 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -1,7 +1,7 @@ context('Awesome Bar', () => { before(() => { cy.visit('/login'); - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); @@ -10,8 +10,9 @@ context('Awesome Bar', () => { }); it('navigates to doctype list', () => { - cy.get('#navbar-search') - .type('todo{downarrow}{enter}', { delay: 100 }); + cy.get('#navbar-search').type('todo', { delay: 200 }); + cy.get('#navbar-search + ul').should('be.visible'); + cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 }); cy.get('h1').should('contain', 'To Do'); @@ -20,7 +21,7 @@ context('Awesome Bar', () => { it('find text in doctype list', () => { cy.get('#navbar-search') - .type('test in todo{downarrow}{enter}', { delay: 100 }); + .type('test in todo{downarrow}{enter}', { delay: 200 }); cy.get('h1').should('contain', 'To Do'); @@ -31,14 +32,14 @@ context('Awesome Bar', () => { it('navigates to new form', () => { cy.get('#navbar-search') - .type('new blog post{downarrow}{enter}', { delay: 100 }); + .type('new blog post{downarrow}{enter}', { delay: 200 }); cy.get('.title-text:visible').should('have.text', 'New Blog Post 1'); }); it('calculates math expressions', () => { cy.get('#navbar-search') - .type('55 + 32{downarrow}{enter}', { delay: 100 }); + .type('55 + 32{downarrow}{enter}', { delay: 200 }); cy.get('.modal-title').should('contain', 'Result'); cy.get('.msgprint').should('contain', '55 + 32 = 87'); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js new file mode 100644 index 0000000000..0524905cce --- /dev/null +++ b/cypress/integration/control_link.js @@ -0,0 +1,75 @@ +context('Control Link', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + cy.create_records({ + doctype: 'ToDo', + description: 'this is a test todo for link' + }).as('todos'); + }); + + function get_dialog_with_link() { + return cy.dialog({ + title: 'Link', + fields: [ + { + 'label': 'Select ToDo', + 'fieldname': 'link', + 'fieldtype': 'Link', + 'options': 'ToDo' + } + ] + }); + } + + it('should set the valid value', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + + cy.get('.frappe-control[data-fieldname=link] input') + .focus() + .type('todo for li') + .type('n', { delay: 600 }) + .type('k', { delay: 700 }); + cy.wait('@search_link'); + cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible'); + cy.get('.frappe-control[data-fieldname=link] input').type('{downarrow}{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=link] input').blur(); + cy.get('@dialog').then(dialog => { + cy.get('@todos').then(todos => { + let value = dialog.get_value('link'); + expect(value).to.eq(todos[0]); + }); + }); + }); + + it.only('should unset invalid value', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + + cy.get('.frappe-control[data-fieldname=link] input') + .type('invalid value', { delay: 100 }) + .blur(); + cy.wait('@validate_link'); + cy.get('.frappe-control[data-fieldname=link] input').should('have.value', ''); + }); + + it('should route to form on arrow click', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + + cy.get('@todos').then(todos => { + cy.get('.frappe-control[data-fieldname=link] input').type(todos[0]).blur(); + cy.wait('@validate_link'); + cy.get('.frappe-control[data-fieldname=link] input').focus(); + cy.get('.frappe-control[data-fieldname=link] .link-btn').click(); + cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`); + }); + }); +}); diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index a367a82273..5fa479b888 100644 --- a/cypress/integration/control_rating.js +++ b/cypress/integration/control_rating.js @@ -1,14 +1,21 @@ -context('Rating Control', () => { - beforeEach(() => { - cy.login('Administrator', 'qwe'); +context('Control Rating', () => { + before(() => { + cy.login(); + cy.visit('/desk'); }); + function get_dialog_with_rating() { + return cy.dialog({ + title: 'Rating', + fields: [{ + 'fieldname': 'rate', + 'fieldtype': 'Rating', + }] + }); + } + it('click on the star rating to record value', () => { - cy.visit('/desk'); - cy.dialog('Rating', [{ - 'fieldname': 'rate', - 'fieldtype': 'Rating', - }]).as('dialog'); + get_dialog_with_rating().as('dialog'); cy.get('div.rating') .children('i.fa') @@ -18,15 +25,13 @@ context('Rating Control', () => { cy.get('@dialog').then(dialog => { var value = dialog.get_value('rate'); expect(value).to.equal(1); + dialog.hide(); }); }); it('hover on the star', () => { - cy.visit('/desk'); - cy.dialog('Rating', [{ - 'fieldname': 'rate', - 'fieldtype': 'Rating', - }]); + get_dialog_with_rating(); + cy.get('div.rating') .children('i.fa') .first() diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index b58e0d49a8..787644b596 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -1,6 +1,6 @@ context('FileUploader', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 9bab715d5b..b7ddd6ecb7 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,6 +1,6 @@ context('Form', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index f4deb66c99..52c6483ab4 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -1,9 +1,9 @@ context('List View', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.test_utils.setup_workflow"); + frappe.call("frappe.tests.ui_test_helpers.setup_workflow"); }); cy.clear_cache(); }); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index 59921b162c..84131386f6 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -1,12 +1,12 @@ context('List View Settings', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); it('Default settings', () => { cy.visit('/desk#List/DocType/List'); cy.get('.list-count').should('contain', "20 of"); - cy.get('.sidebar-stat').should('contain', "No Tags"); + cy.get('.sidebar-stat').should('contain', "Tags"); }); it('disable count and sidebar stats then verify', () => { cy.visit('/desk#List/DocType/List'); @@ -14,13 +14,13 @@ context('List View Settings', () => { cy.get('button').contains('Menu').click(); cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); cy.get('.modal-dialog').should('contain', 'Settings'); - + cy.get('input[data-fieldname="disable_count"]').check({force: true}); cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true}); cy.get('button').filter(':visible').contains('Save').click(); - + cy.reload(); - + cy.get('.list-count').should('be.empty'); cy.get('.list-sidebar .sidebar-stat').should('not.exist'); diff --git a/cypress/integration/login.js b/cypress/integration/login.js index 4da1f39229..3f13130b58 100644 --- a/cypress/integration/login.js +++ b/cypress/integration/login.js @@ -23,7 +23,7 @@ context('Login', () => { it('logs in using correct credentials', () => { cy.get('#login_email').type('Administrator'); - cy.get('#login_password').type('qwe'); + cy.get('#login_password').type(Cypress.config('adminPassword')); cy.get('.btn-login').click(); cy.location('pathname').should('eq', '/desk'); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index 04bc1a6fd4..8aa6279887 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -1,6 +1,6 @@ context('Form', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index f037005dbb..e8b55a9d12 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -1,6 +1,6 @@ context('Recorder', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); }); it('Navigate to Recorder', () => { diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js index c071ce0355..27f594f96e 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_filters.js @@ -1,13 +1,13 @@ context('Relative Timeframe', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.test_utils.create_todo_records"); + frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); }); }); it('set relative filter for Previous and check list', () => { diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index cca9a6eb46..e75baf05f1 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -1,6 +1,6 @@ context('Table MultiSelect', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); }); let name = 'table multiselect' + Math.random().toString().slice(2, 8); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 850898d4c5..84d896dbb0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -25,6 +25,12 @@ import 'cypress-file-upload'; // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); Cypress.Commands.add('login', (email, password) => { + if (!email) { + email = 'Administrator'; + } + if (!password) { + password = Cypress.config('adminPassword'); + } cy.request({ url: '/api/method/login', method: 'POST', @@ -35,6 +41,29 @@ Cypress.Commands.add('login', (email, password) => { }); }); +Cypress.Commands.add('call', (method, args) => { + return cy.window().its('frappe.csrf_token').then(csrf_token => { + return cy.request({ + url: `/api/method/${method}`, + method: 'POST', + body: args, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }).then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('create_records', (doc) => { + return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc }) + .then(r => r.message); +}); + Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => { let selector = `.form-control[data-fieldname="${fieldname}"]`; @@ -72,15 +101,9 @@ Cypress.Commands.add('clear_cache', () => { }); }); -Cypress.Commands.add('dialog', (title, fields) => { - cy.window().then(win => { - var d = new win.frappe.ui.Dialog({ - title: title, - fields: fields, - primary_action: function(){ - d.hide(); - } - }); +Cypress.Commands.add('dialog', (opts) => { + return cy.window().then(win => { + var d = new win.frappe.ui.Dialog(opts); d.show(); return d; }); diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 405de422d3..3d71461739 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -564,10 +564,7 @@ def browse(context, site): site = site.lower() if site in frappe.utils.get_sites(): - webbrowser.open('http://{site}:{port}'.format( - site=site, - port=frappe.get_conf(site).webserver_port - ), new=2) + webbrowser.open(frappe.utils.get_site_url(site), new=2) else: click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site)) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index c9127d94a2..6e0786a528 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -459,26 +459,26 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), sys.exit(ret) @click.command('run-ui-tests') -@click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--test', help="Path to the specific test you want to run") -@click.option('--test-list', help="Path to the txt file with the list of test cases") -@click.option('--profile', is_flag=True, default=False) +@click.argument('app') +@click.option('--headless', is_flag=True, help="Run UI Test in headless mode") @pass_context -def run_ui_tests(context, app=None, test=False, test_list=False, profile=False): +def run_ui_tests(context, app, headless=False): "Run UI tests" - import frappe.test_runner site = get_site(context) - frappe.init(site=site) - frappe.connect() + app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..')) + site_url = frappe.utils.get_site_url(site) + admin_password = frappe.get_conf(site).admin_password - ret = frappe.test_runner.run_ui_tests(app=app, test=test, test_list=test_list, verbose=context.verbose, - profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 + # override baseUrl using env variable + site_env = 'CYPRESS_baseUrl={}'.format(site_url) + password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else '' - if os.environ.get('CI'): - sys.exit(ret) + # run for headless mode + run_or_open = 'run' if headless else 'open' + command = '{site_env} {password_env} yarn run cypress {run_or_open}' + formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) + frappe.commands.popen(formatted_command, cwd=app_base_path) @click.command('run-setup-wizard-ui-test') @click.option('--app', help="App to run tests on, leave blank for all apps") diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 74ed56feb9..5ec8478d60 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -387,7 +387,7 @@ class DocType(Document): os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))]) self.rename_inside_controller(new, old, new_path) - frappe.msgprint('Renamed files and replaced code in controllers, please check!') + frappe.msgprint(_('Renamed files and replaced code in controllers, please check!')) def rename_inside_controller(self, new, old, new_path): for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'): diff --git a/frappe/core/doctype/domain/domain.json b/frappe/core/doctype/domain/domain.json index 3ba8acdc0e..c235596884 100644 --- a/frappe/core/doctype/domain/domain.json +++ b/frappe/core/doctype/domain/domain.json @@ -1,95 +1,54 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:domain", - "beta": 0, - "creation": "2017-05-03 15:07:39.752820", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "field:domain", + "creation": "2017-05-03 15:07:39.752820", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "domain" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "domain", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Domain", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "domain", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Domain", + "reqd": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-09-15 12:26:21.827149", - "modified_by": "Administrator", - "module": "Core", - "name": "Domain", - "name_case": "", - "owner": "makarand@erpnext.com", + ], + "modified": "2019-06-30 13:24:13.732202", + "modified_by": "Administrator", + "module": "Core", + "name": "Domain", + "owner": "makarand@erpnext.com", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "domain", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "domain", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "search_fields": "domain", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "domain" } \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index fff4a5e344..3d8babae6a 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -2,6 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals +from frappe import _ """ record of files @@ -446,7 +447,7 @@ class File(NestedSet): def validate_url(self, df=None): if self.file_url: if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")): - frappe.throw("URL must start with 'http://' or 'https://'") + frappe.throw(_("URL must start with 'http://' or 'https://'")) return self.file_url = unquote(self.file_url) diff --git a/frappe/core/doctype/success_action/success_action.js b/frappe/core/doctype/success_action/success_action.js index b8d56d3c8a..d73d3db326 100644 --- a/frappe/core/doctype/success_action/success_action.js +++ b/frappe/core/doctype/success_action/success_action.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Success Action', { validate: (frm) => { const checked_actions = frm.action_multicheck.get_checked_options(); if (checked_actions.length < 2) { - frappe.msgprint('Select atleast 2 actions'); + frappe.msgprint(__('Select atleast 2 actions')); } else { return true; } diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index a0b553c43a..3e822f0007 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -166,7 +166,7 @@ frappe.listview_settings['User Permission'] = { return data; } if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { - frappe.throw("Please select applicable Doctypes"); + frappe.throw(__("Please select applicable Doctypes")); } return data; }, diff --git a/frappe/database/database.py b/frappe/database/database.py index 3aab284de0..7650af43f9 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -924,7 +924,7 @@ class Database(object): conditions=conditions ), values) else: - frappe.throw('No conditions provided') + frappe.throw(_('No conditions provided')) def log_touched_tables(self, query, values=None): if values: diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index ff54f95031..53d1110e83 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe, json +from frappe import _ from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate from frappe.model.document import Document @@ -199,6 +200,6 @@ class DashboardChart(Document): def check_required_field(self): if not self.based_on: - frappe.throw("Time series based on is required to create a dashboard chart") + frappe.throw(_("Time series based on is required to create a dashboard chart")) if not self.document_type: - frappe.throw("Document type is required to create a dashboard chart") \ No newline at end of file + frappe.throw(_("Document type is required to create a dashboard chart")) \ No newline at end of file diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 1ad57246a6..e88c11b4f8 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -71,7 +71,7 @@ class Event(Document): communication.communication_date = self.starts_on communication.reference_doctype = self.doctype communication.reference_name = self.name - communication.communication_medium = communication_mapping[self.event_category] if self.event_category else "" + communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else "" communication.status = "Linked" communication.add_link(participant.reference_doctype, participant.reference_docname) communication.save(ignore_permissions=True) diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index b0d9a5d123..da7c7a3a9e 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -18,7 +18,6 @@ def remove_attach(): file_name = frappe.form_dict.get('file_name') frappe.delete_doc('File', fid) - @frappe.whitelist() def validate_link(): """validate link when updated by user""" @@ -84,27 +83,23 @@ def update_comment(name, content): doc.save(ignore_permissions=True) @frappe.whitelist() -def get_next(doctype, value, prev, filters=None, order_by="modified desc"): - - prev = not int(prev) - sort_field, sort_order = order_by.split(" ") +def get_next(doctype, value, prev, filters, sort_order, sort_field): + prev = int(prev) if not filters: filters = [] if isinstance(filters, string_types): filters = json.loads(filters) - # condition based on sort order - condition = ">" if sort_order.lower()=="desc" else "<" + # # condition based on sort order + condition = ">" if sort_order.lower() == "asc" else "<" # switch the condition if prev: - condition = "<" if condition==">" else "<" - else: - sort_order = "asc" if sort_order.lower()=="desc" else "desc" + sort_order = "asc" if sort_order.lower() == "desc" else "desc" + condition = "<" if condition == ">" else ">" - # add condition for next or prev item - if not order_by[0] in [f[1] for f in filters]: - filters.append([doctype, sort_field, condition, value]) + # # add condition for next or prev item + filters.append([doctype, sort_field, condition, frappe.get_value(doctype, value, sort_field)]) res = frappe.get_list(doctype, fields = ["name"], diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index f31aae401e..f4927dd098 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -3,8 +3,6 @@ from __future__ import unicode_literals import frappe -import json - @frappe.whitelist() def get_list_settings(doctype): @@ -22,31 +20,37 @@ def set_list_settings(doctype, values): doc = frappe.new_doc("List View Setting") doc.name = doctype frappe.clear_messages() - doc.update(json.loads(values)) + doc.update(frappe.parse_json(values)) doc.save() + @frappe.whitelist() -def get_user_assignments_and_count(doctype, current_filters): - +def get_group_by_count(doctype, current_filters, field): + current_filters = frappe.parse_json(current_filters) subquery_condition = '' - if current_filters: - # get the subquery - subquery = frappe.get_all(doctype, - filters=current_filters, return_query = True) + + subquery = frappe.get_all(doctype, filters=current_filters, return_query = True) + if field == 'assigned_to': subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery) + return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count + from + `tabToDo`, `tabUser` + where + `tabToDo`.status='Open' and + `tabToDo`.owner = `tabUser`.name and + `tabUser`.user_type = 'System User' + {subquery_condition} + group by + `tabToDo`.owner + order by + count desc + limit 50""".format(subquery_condition = subquery_condition), as_dict=True) + else : + return frappe.db.get_list(doctype, + filters=current_filters, + group_by=field, + fields=['count(*) as count', field + ' as name'], + order_by='count desc', + limit=50, + ) - todo_list = frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count - from - `tabToDo`, `tabUser` - where - `tabToDo`.status='Open' and - `tabToDo`.owner = `tabUser`.name and - `tabUser`.user_type = 'System User' - {subquery_condition} - group by - `tabToDo`.owner - order by - count desc - limit 50""".format(subquery_condition = subquery_condition), as_dict=True) - - return todo_list \ No newline at end of file diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index fa131c9c02..9a950a694d 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -213,7 +213,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } setTimeout(function() { // Reload - window.location.href = ''; + window.location.href = '/desk'; }, 2000); } diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index caf6a7d4f0..9d6f3561cb 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -307,6 +307,7 @@ def export_query(): if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + include_indentation = data["include_indentation"] if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) else: @@ -318,7 +319,7 @@ def export_query(): columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx - xlsx_data = build_xlsx_data(columns, data, visible_idx) + xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation) xlsx_file = make_xlsx(xlsx_data, "Query Report") frappe.response['filename'] = report_name + '.xlsx' @@ -326,7 +327,7 @@ def export_query(): frappe.response['type'] = 'binary' -def build_xlsx_data(columns, data, visible_idx): +def build_xlsx_data(columns, data, visible_idx,include_indentation): result = [[]] # add column headings @@ -344,7 +345,7 @@ def build_xlsx_data(columns, data, visible_idx): label = columns[idx]["label"] fieldname = columns[idx]["fieldname"] cell_value = row.get(fieldname, row.get(label, "")) - if 'indent' in row and idx == 0: + if cint(include_indentation) and 'indent' in row and idx == 0: cell_value = (' ' * cint(row['indent'])) + cell_value row_data.append(cell_value) else: diff --git a/frappe/hooks.py b/frappe/hooks.py index 10e1c36881..c81d74e68a 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -18,8 +18,8 @@ app_email = "info@frappe.io" docs_app = "frappe_io" -translation_contribution_url = "https://translate.erpnext.xyz/api/method/translator.api.add_translation" -translation_contribution_status = "https://translate.erpnext.xyz/api/method/translator.api.translation_status" +translation_contribution_url = "https://translate.erpnext.com/api/method/translator.api.add_translation" +translation_contribution_status = "https://translate.erpnext.com/api/method/translator.api.translation_status" before_install = "frappe.utils.install.before_install" after_install = "frappe.utils.install.after_install" diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index c264d12dec..ea731fafc2 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -37,7 +37,8 @@ frappe.ui.form.on('Dropbox Settings', { }, take_backup: function(frm) { - if ((frm.doc.app_access_key && frm.doc.app_secret_key) || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)){ + if (frm.doc.enabled && ((frm.doc.app_access_key && frm.doc.app_secret_key) + || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))) { frm.add_custom_button(__("Take Backup Now"), function(frm){ frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 0ea65d7c68..6610173e3e 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -135,23 +135,24 @@ def sync(g_contact=None): for name in connection.get("names"): if name.get("metadata").get("primary"): - for email in connection.get("emailAddresses"): - if not frappe.db.exists("Contact", {"email_id": email.get("value")}): - contacts_updated += 1 + if connection.get("emailAddresses"): + for email in connection.get("emailAddresses"): + if not frappe.db.exists("Contact", {"email_id": email.get("value")}): + contacts_updated += 1 - frappe.get_doc({ - "doctype": "Contact", - "salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "", - "first_name": name.get("givenName") if name.get("givenName") else "", - "middle_name": name.get("middleName") if name.get("middleName") else "", - "last_name": name.get("familyName") if name.get("familyName") else "", - "email_id": email.get("value") if email.get("value") else "", - "designation": get_indexed_value(connection.get("organizations"), 0, "title"), - "phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"), - "mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"), - "source": "Google Contacts", - "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") - }).insert(ignore_permissions=True) + frappe.get_doc({ + "doctype": "Contact", + "salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "", + "first_name": name.get("givenName") if name.get("givenName") else "", + "middle_name": name.get("middleName") if name.get("middleName") else "", + "last_name": name.get("familyName") if name.get("familyName") else "", + "email_id": email.get("value") if email.get("value") else "", + "designation": get_indexed_value(connection.get("organizations"), 0, "title"), + "phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"), + "mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"), + "source": "Google Contacts", + "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") + }).insert(ignore_permissions=True) if g_contact: return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.") diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 115d175379..96814984f8 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -477,7 +477,7 @@ def get_field_currency(df, doc=None): if ":" in cstr(df.get("options")): split_opts = df.get("options").split(":") - if len(split_opts)==3: + if len(split_opts)==3 and doc.get(split_opts[1]): currency = frappe.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2]) else: currency = doc.get(df.get("options")) diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index e5d329cd79..efbe46a4ab 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals, print_function from six.moves import range import frappe +from frappe import _ from frappe.utils import cstr from frappe.build import html_to_js_template import re @@ -62,7 +63,7 @@ def render_include(content): if "{% include" in content: paths = re.findall(r'''{% include\s['"](.*)['"]\s%}''', content) if not paths: - frappe.throw('Invalid include path', InvalidIncludePath) + frappe.throw(_('Invalid include path'), InvalidIncludePath) for path in paths: app, app_path = path.split('/', 1) diff --git a/frappe/patches.txt b/frappe/patches.txt index ac3b3501f5..74a8656379 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -246,3 +246,4 @@ frappe.patches.v11_0.apply_customization_to_custom_doctype frappe.patches.v12_0.remove_feedback_rating frappe.patches.v12_0.move_form_attachments_to_attachments_folder frappe.patches.v12_0.move_timeline_links_to_dynamic_links +frappe.patches.v12_0.delete_feedback_request_if_exists #1 diff --git a/frappe/patches/v12_0/delete_feedback_request_if_exists.py b/frappe/patches/v12_0/delete_feedback_request_if_exists.py new file mode 100644 index 0000000000..fdbcecfc5a --- /dev/null +++ b/frappe/patches/v12_0/delete_feedback_request_if_exists.py @@ -0,0 +1,8 @@ + +import frappe + +def execute(): + frappe.db.sql(''' + DELETE from `tabDocType` + WHERE name = 'Feedback Request' + ''') \ No newline at end of file diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index 2758f0aa9c..9bb87ab311 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -19,7 +19,7 @@ class PrintSettings(Document): try: import cups except ImportError: - frappe.throw("You need to install pycups to use this feature!") + frappe.throw(_("You need to install pycups to use this feature!")) return try: cups.setServer(self.server_ip) diff --git a/frappe/public/build.json b/frappe/public/build.json index 203a642195..d4defadac7 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -276,6 +276,7 @@ "public/js/frappe/list/list_sidebar.js", "public/js/frappe/list/list_sidebar.html", "public/js/frappe/list/list_sidebar_stat.html", + "public/js/frappe/list/list_sidebar_group_by.js", "public/js/frappe/list/list_view_permission_restrictions.html", "public/js/frappe/views/gantt/gantt_view.js", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 4dc3b4b0f8..3f4e391461 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -464,6 +464,11 @@ frappe.Application = Class.extend({ return frappe.call('frappe.client.get_hooks', { hook: 'app_logo_url' }) .then(r => { frappe.app.logo_url = (r.message || []).slice(-1)[0]; + if (window.cordova) { + let host = frappe.request.url; + host = host.slice(0, host.length - 1); + frappe.app.logo_url = host + frappe.app.logo_url; + } }); }, diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 27fe04e9dc..68136eba44 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -125,7 +125,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ if(!d.label) { d.label = d.value; } var _label = (me.translate_values) ? __(d.label) : d.label; - var html = "" + _label + ""; + var html = d.html || "" + _label + ""; if(d.description && d.value!==d.description) { html += '
' + __(d.description) + ''; } @@ -174,32 +174,12 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ // show filter description in awesomplete if (args.filters) { - let filter_string = []; - - if (Array.isArray(args.filters)) { - let filters = args.filters; - - filters.forEach((filter) => { - filter_string.push(`${frappe.model.unscrub(filter[1])} ${filter[2]} ${filter[3]}`); - }); - } else { - for (let [key, value] of Object.entries(args.filters)) { - if (Array.isArray(value) && value[1]) { - filter_string.push(`${frappe.model.unscrub(key)} ${value[0]} ${value[1]}`); - } else if (value) { - filter_string.push(`${frappe.model.unscrub(key)} as ${value}`); - } - } - } - - if (filter_string.length > 0) { - filter_string = "Filters applied for " + filter_string.join(", "); - + let filter_string = me.get_filter_description(args.filters); + if (filter_string) { r.results.push({ - label: "" - + __("{0}", [filter_string]) - + "", - value: "" + html: `${filter_string}`, + value: '', + action: () => {} }); } } @@ -234,7 +214,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ me.awesomplete.list = me.$input.cache[doctype][term]; } }); - }, 618)); + }, 500)); this.$input.on("blur", function() { if(me.selected) { @@ -250,8 +230,6 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.$input.on("awesomplete-open", function() { me.$wrapper.css({"z-index": 100}); me.$wrapper.find('ul').css({"z-index": 100}); - me.$wrapper.find('.disable-select').parents('li').css({"pointer-events": "none"}); - me.$wrapper.find('.disable-select').unwrap(); me.autocomplete_open = true; }); @@ -297,6 +275,57 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } }); }, + + get_filter_description(filters) { + let doctype = this.get_options(); + let filter_array = []; + let meta = null; + + frappe.model.with_doctype(doctype, () => { + meta = frappe.get_meta(doctype); + }); + + // convert object style to array + if (!Array.isArray(filters)) { + for (let fieldname in filters) { + let value = filters[fieldname]; + if (!Array.isArray(value)) { + value = ['=', value]; + } + filter_array.push([fieldname, ...value]); // fieldname, operator, value + } + } else { + filter_array = filters; + } + + // add doctype if missing + filter_array = filter_array.map(filter => { + if (filter.length === 3) { + return [doctype, ...filter]; // doctype, fieldname, operator, value + } + return filter; + }); + + function get_filter_description(filter) { + let doctype = filter[0]; + let fieldname = filter[1]; + let docfield = frappe.meta.get_docfield(doctype, fieldname); + let label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + + let value = filter[3] == null || filter[3] === '' + ? __('empty') + : String(filter[3]); + + return [__(label).bold(), filter[2], value.bold()].join(' '); + } + + let filter_string = filter_array + .map(get_filter_description) + .join(', '); + + return __('Filters applied for {0}', [filter_string]); + }, + set_custom_query: function(args) { var set_nulls = function(obj) { $.each(obj, function(key, value) { diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index e244c3c12e..7592b80a62 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -69,6 +69,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.set_input_attributes(); this.values = []; + this._options = []; + this._selected_values = []; this.highlighted = -1; }, @@ -104,6 +106,20 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.update_status(); }, + set_value(value) { + if (!value) return Promise.resolve(); + if (typeof value === 'string') { + value = [value]; + } + this.values = value; + this.values.forEach(value => { + this.update_selected_values(value); + }); + this.parse_validate_and_set_in_model(''); + this.update_status(); + return Promise.resolve(); + }, + update_selected_values(value) { this._selected_values = this._selected_values || []; let option = this._options.find(opt => opt.value === value); @@ -122,7 +138,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ text = this.get_placeholder_text(); } else if (this.values.length === 1) { let val = this.values[0]; - text = this._options.find(opt => opt.value === val).label; + let option = this._options.find(opt => opt.value === val); + text = option ? option.label : val; } else { text = __('{0} values selected', [this.values.length]); } diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 5fad73364d..85af73823a 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -37,7 +37,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ if (data.length === 1 & data[0].length === 1) return; if (data.length > 100){ data = data.slice(0, 100); - frappe.msgprint('for performance, only the first 100 rows processed!'); + frappe.msgprint(__('For performance, only the first 100 rows were processed.')); } var fieldnames = []; var get_field = function(name_or_label){ diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 33c8fcf3dc..1e4e063204 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -416,7 +416,7 @@ frappe.ui.form.Dashboard = Class.extend({ update_heatmap: function(data) { if(this.heatmap) { - this.heatmap.update(data); + this.heatmap.update({dataPoints: data}); } }, diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index b7eec630ce..2a6813d9da 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -74,8 +74,8 @@ frappe.ui.form.Timeline = class Timeline { }); }); - this.email_link.on("click", ".copy-to-clipboard", function() { - let text = $(".copy-to-clipboard").text(); + this.email_link.on("click", function(e) { + let text = $(e.currentTarget).find(".copy-to-clipboard").text(); frappe.utils.copy_to_clipboard(text); }); } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 28f4fb8a3a..24b9aed646 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -87,6 +87,9 @@ frappe.ui.form.Form = class FrappeForm { page: this.page }); + // navigate records keyboard shortcuts + this.add_nav_keyboard_shortcuts(); + // print layout this.setup_print_layout(); @@ -112,6 +115,24 @@ frappe.ui.form.Form = class FrappeForm { this.setup_done = true; } + add_nav_keyboard_shortcuts() { + frappe.ui.keys.add_shortcut({ + shortcut: 'shift+>', + action: () => this.navigate_records(0), + page: this.page, + description: __('Go to next record'), + condition: () => !this.is_new() + }); + + frappe.ui.keys.add_shortcut({ + shortcut: 'shift+<', + action: () => this.navigate_records(1), + page: this.page, + description: __('Go to previous record'), + condition: () => !this.is_new() + }); + } + setup_print_layout() { this.print_preview = new frappe.ui.form.PrintPreview({ frm: this @@ -521,7 +542,9 @@ frappe.ui.form.Form = class FrappeForm { me.script_manager.trigger("after_save"); // submit comment if entered - me.timeline.comment_area.submit(); + if (me.timeline) { + me.timeline.comment_area.submit(); + } me.refresh(); } else { if(on_error) { @@ -797,6 +820,24 @@ frappe.ui.form.Form = class FrappeForm { this.print_preview.toggle(); } + navigate_records(prev) { + let list_settings = frappe.get_user_settings(this.doctype)['List']; + let args = { + doctype: this.doctype, + value: this.docname, + filters: list_settings.filters, + sort_order: list_settings.sort_order, + sort_field: list_settings.sort_by, + prev, + }; + + frappe.call('frappe.desk.form.utils.get_next', args).then(r => { + if (r.message) { + frappe.set_route('Form', this.doctype, r.message); + } + }); + } + rename_doc() { frappe.model.rename_doc(this.doctype, this.docname); } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 72e6f1ae98..2dfca08b0d 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -253,6 +253,7 @@ export default class Grid { // toolbar this.setup_toolbar(); + this.toggle_checkboxes(this.display_status !== 'Read'); // sortable if(this.frm && this.is_sortable() && !this.sortable_setup_done) { @@ -445,6 +446,9 @@ export default class Grid { this.get_docfield(fieldname).hidden = show ? 0 : 1; this.refresh(); } + toggle_checkboxes(enable) { + this.wrapper.find(".grid-row-check").prop('disabled', !enable) + } get_docfield(fieldname) { return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); } @@ -780,4 +784,4 @@ export default class Grid { // hide all custom buttons this.grid_buttons.find('.btn-custom').addClass('hidden'); } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 92f34aa623..0f3f01ac61 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -606,7 +606,7 @@ export default class GridRow { } } - get_visible_columns(blacklist) { + get_visible_columns(blacklist=[]) { var me = this; var visible_columns = $.map(this.docfields, function(df) { var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 89298607c1..0d8d2caca1 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -35,6 +35,20 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ 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"}); @@ -142,6 +156,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ clearTimeout($this.data('timeout')); $this.data('timeout', setTimeout(function() { frappe.flags.auto_scroll = false; + me.empty_list(); me.get_results(); }, 300)); }); @@ -198,16 +213,15 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ render_result_list: function(results, more = 0) { var me = this; - var more_btn = me.dialog.fields_dict.more_btn.$wrapper; // Make empty result set if filter is set if (!frappe.flags.auto_scroll) { - this.$results.empty(); + this.empty_list(); } if(results.length === 0) { - this.$results.empty(); + this.empty_list(); more_btn.hide(); return; } else if(more) { @@ -223,6 +237,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ } }, + empty_list: function() { + this.$results.find('.list-item-container').remove(); + }, + get_results: function() { let me = this; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 53dc40925c..38250f2ad8 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -159,17 +159,29 @@ frappe.ui.form.QuickEntryForm = Class.extend({ doc: me.dialog.doc }, callback: function(r) { - me.dialog.hide(); - // delete the old doc - frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); - me.dialog.doc = r.message; - if(frappe._from_link) { - frappe.ui.form.update_calling_link(me.dialog.doc); + + if (frappe.model.is_submittable(me.doctype)) { + frappe.run_serially([ + () => me.dialog.working = true, + () => { + me.dialog.set_primary_action(__('Submit'), function() { + me.submit(r.message); + }); + } + ]); } else { - if(me.after_insert) { - me.after_insert(me.dialog.doc); + me.dialog.hide(); + // delete the old doc + frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); + me.dialog.doc = r.message; + if(frappe._from_link) { + frappe.ui.form.update_calling_link(me.dialog.doc); } else { - me.open_form_if_not_list(); + if(me.after_insert) { + me.after_insert(me.dialog.doc); + } else { + me.open_form_if_not_list(); + } } } }, @@ -185,6 +197,26 @@ frappe.ui.form.QuickEntryForm = Class.extend({ }); }, + submit: function(doc) { + var me = this; + frappe.call({ + method: "frappe.client.submit", + args : { + doc: doc + }, + callback: function(r) { + me.dialog.hide(); + // delete the old doc + frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); + me.dialog.doc = r.message; + if (frappe._from_link) { + frappe.ui.form.update_calling_link(me.dialog.doc); + } + cur_frm.reload_doc(); + } + }); + }, + open_form_if_not_list: function() { let route = frappe.get_route(); let doc = this.dialog.doc; diff --git a/frappe/public/js/frappe/form/sidebar/user_image.js b/frappe/public/js/frappe/form/sidebar/user_image.js index 8be760179a..6c8099db89 100644 --- a/frappe/public/js/frappe/form/sidebar/user_image.js +++ b/frappe/public/js/frappe/form/sidebar/user_image.js @@ -4,6 +4,7 @@ frappe.ui.form.set_user_image = function(frm) { var image_field = frm.meta.image_field; var image = frm.doc[image_field]; var title_image = frm.page.$title_area.find('.title-image'); + var image_actions = frm.sidebar.image_wrapper.find('.sidebar-image-actions'); image_section.toggleClass('hide', image_field ? false : true); @@ -32,6 +33,8 @@ frappe.ui.form.set_user_image = function(frm) { .css("background-image", 'url("' + image + '")') .html(''); + image_actions.find('.sidebar-image-change, .sidebar-image-remove').show(); + } else { image_section .find(".sidebar-image") @@ -51,6 +54,8 @@ frappe.ui.form.set_user_image = function(frm) { .css({'background-color': frappe.get_palette(title)}) .html(frappe.get_abbr(title)); + image_actions.find('.sidebar-image-change').show(); + image_actions.find('.sidebar-image-remove').hide(); } } @@ -63,12 +68,27 @@ frappe.ui.form.setup_user_image_event = function(frm) { }); } - // bind click on image_wrapper - frm.sidebar.image_wrapper.on('click', function() { - var field = frm.get_field(frm.meta.image_field); - if(!field.$input) { - field.make_input(); + frm.sidebar.image_wrapper.on('click', ':not(.sidebar-image-actions)', (e) => { + let $target = $(e.currentTarget); + if ($target.is('a.dropdown-toggle, .dropdown')) { + return; + } + let dropdown = frm.sidebar.image_wrapper.find('.sidebar-image-actions .dropdown'); + dropdown.toggleClass('open'); + e.stopPropagation(); + }); + + // bind click on image_wrapper + frm.sidebar.image_wrapper.on('click', '.sidebar-image-change, .sidebar-image-remove', function(e) { + let $target = $(e.currentTarget); + var field = frm.get_field(frm.meta.image_field); + if ($target.is('.sidebar-image-change')) { + if(!field.$input) { + field.make_input(); + } + field.$input.trigger('click'); + } else { + field.set_value('').then(() => frm.save()); } - field.$input.trigger('click'); }); } \ No newline at end of file diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index 151adbf22f..b611557c43 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -12,6 +12,15 @@ + {% if frm.meta.beta %} diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 3eebe2e371..122c0d9452 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -129,7 +129,10 @@ frappe.ui.form.Toolbar = Class.extend({ if(frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) { this.page.add_menu_item(__("Email"), function() { me.frm.email_doc(); - }, true, 'Ctrl+E'); + }, true, { + shortcut: 'Ctrl+E', + condition: () => !this.frm.is_new() + }); } // go to field modal @@ -168,7 +171,10 @@ frappe.ui.form.Toolbar = Class.extend({ && frappe.model.can_delete(me.frm.doctype)) { this.page.add_menu_item(__("Delete"), function() { me.frm.savetrash(); - }, true, 'Shift+Ctrl+D'); + }, true, { + shortcut: 'Shift+Ctrl+D', + condition: () => !this.frm.is_new() + }); } if(frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) { @@ -197,7 +203,20 @@ frappe.ui.form.Toolbar = Class.extend({ if(p[CREATE] && !this.frm.meta.issingle) { this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { frappe.new_doc(me.frm.doctype, true); - }, true, 'Ctrl+B'); + }, true, { + shortcut: 'Ctrl+B', + condition: () => !this.frm.is_new() + }); + } + + // Navigate + if(!this.frm.is_new() && !issingle) { + this.page.add_action_icon("fa fa-chevron-left prev-doc", function() { + me.frm.navigate_records(1); + }); + this.page.add_action_icon("fa fa-chevron-right next-doc", function() { + me.frm.navigate_records(0); + }); } }, can_repeat: function() { diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index c8c2239b03..885b5f2dae 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -52,21 +52,31 @@ - {% if(frappe.help.has_help(doctype)) { %}
  • {{ __("Help") }}
  • {% } %} + + +