From 82b98330fd0f0cbfcbd62b6775fc3955796e4d01 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 14 Apr 2021 19:50:08 +0530 Subject: [PATCH 001/515] feat: Add URL option for data type fields --- frappe/model/__init__.py | 3 ++- frappe/model/base_document.py | 3 +++ frappe/public/js/frappe/form/controls/data.js | 3 +++ frappe/public/js/frappe/utils/datatype.js | 4 ++++ frappe/utils/__init__.py | 15 ++++++++++++++- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index af06696621..205b451336 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -71,7 +71,8 @@ numeric_fieldtypes = ( data_field_options = ( 'Email', 'Name', - 'Phone' + 'Phone', + 'URL' ) default_fields = ( diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 983511f7a4..cf63aa98b6 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -666,6 +666,9 @@ class BaseDocument(object): if data_field_options == "Phone": frappe.utils.validate_phone_number(data, throw=True) + if data_field_options == "URL": + frappe.utils.validate_url(data, throw=True) + def _validate_constants(self): if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants: return diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index f381d1b4a2..b4d24d9a8f 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -126,6 +126,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.df.invalid = email_invalid; return v; } + } else if (this.df.options == 'URL') { + this.df.invalid = !validate_url(v); + return v; } else { return v; } diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index 1b9206f434..ad0fd4324c 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -52,6 +52,10 @@ window.validate_name = function(txt) { return frappe.utils.validate_type(txt, "name"); }; +window.validate_url = function(txt) { + return frappe.utils.validate_type(txt, "url"); +} + window.nth = function(number) { number = cint(number); var s = 'th'; diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index efa69d4453..3e397afee6 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -19,7 +19,7 @@ from gzip import GzipFile from typing import Generator, Iterable from six import string_types, text_type -from six.moves.urllib.parse import quote +from six.moves.urllib.parse import quote, urlparse from werkzeug.test import Client import frappe @@ -161,6 +161,19 @@ def split_emails(txt): return email_list +def validate_url(txt, throw=False): + try: + url = urlparse(txt).netloc + if not url: + raise frappe.ValidationError + except Exception as e: + if throw: + frappe.throw( + frappe._("'{0}' is not a valid URL").format(txt) + ) + + return False + def random_string(length): """generate a random string""" import string From ce2dabed78b5da6e4b428d600c208ec8fc6e1a14 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 15 Apr 2021 06:02:29 +0530 Subject: [PATCH 002/515] fix: Call to translate function --- frappe/utils/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 3e397afee6..5992fdb6db 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -169,7 +169,9 @@ def validate_url(txt, throw=False): except Exception as e: if throw: frappe.throw( - frappe._("'{0}' is not a valid URL").format(txt) + frappe._( + "'{0}' is not a valid URL" + ).format('' + +'') ) return False From 4d91f318f94781a74e2b0c6361460966e79ab154 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 19 Apr 2021 12:32:19 +0530 Subject: [PATCH 003/515] test: UI with form validation --- .../fixtures/data_field_validation_doctype.js | 57 +++++++++++++++++++ .../integration/data_field_form_validation.js | 39 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 cypress/fixtures/data_field_validation_doctype.js create mode 100644 cypress/integration/data_field_form_validation.js diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js new file mode 100644 index 0000000000..75fa88554e --- /dev/null +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -0,0 +1,57 @@ +export default { + name: 'Validation Test', + custom: 1, + actions: [], + creation: '2019-03-15 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'email', + fieldtype: 'Data', + label: 'Email', + options: 'Email' + }, + { + fieldname: 'URL', + fieldtype: 'Data', + label: 'URL', + options: 'URL' + }, + { + fieldname: 'Phone', + fieldtype: 'Data', + label: 'Phone', + options: 'Phone' + }, + { + fieldname: 'person_name', + fieldtype: 'Data', + label: 'Person Name', + options: 'Name' + } + ], + issingle: 1, + links: [], + modified: '2021-04-19 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1 + } + ], + quick_entry: 1, + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js new file mode 100644 index 0000000000..17a7f8e154 --- /dev/null +++ b/cypress/integration/data_field_form_validation.js @@ -0,0 +1,39 @@ +import data_field_validation_doctype from '../fixtures/data_field_validation_doctype'; +const doctype_name = data_field_validation_doctype.name; + + +context('Data Field Input Validation in New Form', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + return cy.insert_doc('DocType', data_field_validation_doctype, true); + }); + + function validateField(fieldname, invalid_value, valid_value) { + // Invalid, should have has-error class + cy.get_field(fieldname).type(invalid_value).blur(); + cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error'); + // Valid value, should not have has-error class + cy.get_field(fieldname).clear().type(valid_value); + cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error'); + } + + describe('Data Field Options', () => { + it('should validate email address', () => { + cy.new_form(doctype_name); + validateField('email', 'captian', 'hello@test.com'); + }); + + it('should validate URL', () => { + validateField('url', 'jkl', 'https://frappe.io'); + }); + + it('should validate phone number', () => { + validateField('phone', 'america', '89787878'); + }); + + it('should validate name', () => { + validateField('person_name', ' 777Hello', 'James Bond'); + }); + }); +}); \ No newline at end of file From ea38895f1a45cd1c163254d10cfac1d5c3284cce Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 19 Apr 2021 13:17:20 +0530 Subject: [PATCH 004/515] fix: Sider Issues --- .eslintrc | 2 ++ cypress/fixtures/data_field_validation_doctype.js | 4 ++-- cypress/integration/data_field_form_validation.js | 4 ++-- frappe/public/js/frappe/utils/datatype.js | 2 +- frappe/utils/__init__.py | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index d123023a68..2d17d7937b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -80,6 +80,7 @@ "validate_email": true, "validate_name": true, "validate_phone": true, + "validate_url": true, "get_number_format": true, "format_number": true, "format_currency": true, @@ -144,6 +145,7 @@ "cy": true, "it": true, "expect": true, + "describe": true, "context": true, "before": true, "beforeEach": true, diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js index 75fa88554e..469ff8ca24 100644 --- a/cypress/fixtures/data_field_validation_doctype.js +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -11,13 +11,13 @@ export default { fieldname: 'email', fieldtype: 'Data', label: 'Email', - options: 'Email' + options: 'Email' }, { fieldname: 'URL', fieldtype: 'Data', label: 'URL', - options: 'URL' + options: 'URL' }, { fieldname: 'Phone', diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js index 17a7f8e154..e6f6f13df6 100644 --- a/cypress/integration/data_field_form_validation.js +++ b/cypress/integration/data_field_form_validation.js @@ -18,7 +18,7 @@ context('Data Field Input Validation in New Form', () => { cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error'); } - describe('Data Field Options', () => { + describe('Data Field Options', () => { it('should validate email address', () => { cy.new_form(doctype_name); validateField('email', 'captian', 'hello@test.com'); @@ -35,5 +35,5 @@ context('Data Field Input Validation in New Form', () => { it('should validate name', () => { validateField('person_name', ' 777Hello', 'James Bond'); }); - }); + }); }); \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index ad0fd4324c..944d3fca1a 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -54,7 +54,7 @@ window.validate_name = function(txt) { window.validate_url = function(txt) { return frappe.utils.validate_type(txt, "url"); -} +}; window.nth = function(number) { number = cint(number); diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 5992fdb6db..2fdaa05404 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -166,12 +166,12 @@ def validate_url(txt, throw=False): url = urlparse(txt).netloc if not url: raise frappe.ValidationError - except Exception as e: + except Exception: if throw: frappe.throw( frappe._( "'{0}' is not a valid URL" - ).format('' + +'') + ).format('' + txt +'') ) return False From f84aee8abe858dd61210c21aa5b6d1642dba6ec1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 18:16:59 +0200 Subject: [PATCH 005/515] fix: translate report column labels --- frappe/translate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/translate.py b/frappe/translate.py index 3565bbc32c..5989ff44aa 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -443,8 +443,12 @@ def get_messages_from_report(name): messages = _get_messages_from_page_or_report("Report", name, frappe.db.get_value("DocType", report.ref_doctype, "module")) + if report.columns: + messages.extend([(None, column.label) for column in report.columns]) + if report.query: messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)]) + messages.append((None,report.report_name)) return messages From bf4a73c3d4890a42016d49a34d455aad18307068 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 18:28:02 +0200 Subject: [PATCH 006/515] fix: translate report filter labels --- frappe/translate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 5989ff44aa..a4128794ac 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -444,7 +444,10 @@ def get_messages_from_report(name): frappe.db.get_value("DocType", report.ref_doctype, "module")) if report.columns: - messages.extend([(None, column.label) for column in report.columns]) + messages.extend([(None, report_column.label) for report_column in report.columns]) + + if report.filters: + messages.extend([(None, report_filter.label) for report_filter in report.filters]) if report.query: messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)]) From 6250c4ac9d62b943936e92cef15627534a8a1480 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 19:46:33 +0200 Subject: [PATCH 007/515] fix: add context to filter columns --- frappe/public/js/frappe/views/reports/query_report.js | 2 +- frappe/translate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 834946b437..4e50210d0a 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1094,7 +1094,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return Object.assign(column, { id: column.fieldname, - name: __(column.label), + name: __(column.label, null, `Column of report '${this.report_name}'`), // context has to match context in get_messages_from_report in translate.py width: parseInt(column.width) || null, editable: false, compareValue: compareFn, diff --git a/frappe/translate.py b/frappe/translate.py index a4128794ac..49e4a0855c 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -444,7 +444,8 @@ def get_messages_from_report(name): frappe.db.get_value("DocType", report.ref_doctype, "module")) if report.columns: - messages.extend([(None, report_column.label) for report_column in report.columns]) + context = "Column of report '%s'" % report.name # context has to match context in `prepare_columns` in query_report.js + messages.extend([(None, report_column.label, context) for report_column in report.columns]) if report.filters: messages.extend([(None, report_filter.label) for report_filter in report.filters]) From 9dc4cac49571144b90ce34a27ebc87b34fd7653b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 00:30:00 +0530 Subject: [PATCH 008/515] fix: Use grid docfield list while creating grid_row docfield copy (backport #12940) (#12946) Previously, it was using doctype level docfield list which did not had the updated docfields for a grid. (cherry picked from commit acfa1c1cca561c8e4880b0eaf8e554158f1c5656) Co-authored-by: Suraj Shetty --- frappe/public/js/frappe/form/grid_row.js | 1 + frappe/public/js/frappe/model/meta.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index e0fe1b3b54..4afa251c27 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -6,6 +6,7 @@ export default class GridRow { this.on_grid_fields = []; $.extend(this, opts); if (this.doc && this.parent_df.options) { + frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); } this.columns = {}; diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index c2fd6b1ae6..6ee9084adc 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -38,14 +38,14 @@ $.extend(frappe.meta, { frappe.meta.docfield_list[df.parent].push(df); }, - make_docfield_copy_for: function(doctype, docname) { + make_docfield_copy_for: function(doctype, docname, docfield_list=null) { var c = frappe.meta.docfield_copy; if(!c[doctype]) c[doctype] = {}; if(!c[doctype][docname]) c[doctype][docname] = {}; - var docfield_list = frappe.meta.docfield_list[doctype] || []; + docfield_list = docfield_list || frappe.meta.docfield_list[doctype] || []; for(var i=0, j=docfield_list.length; i Date: Thu, 22 Apr 2021 00:40:46 +0530 Subject: [PATCH 009/515] fix: Form Dashboard reference link (backport #12945) (#12947) (cherry picked from commit 4d552c241f7b9a2e6b9a5dfa9bd6d430a6f2cbac) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/form/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index ed3ad5ea09..55c965db62 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -290,7 +290,7 @@ frappe.ui.form.Dashboard = class FormDashboard { // bind links transactions_area_body.find(".badge-link").on('click', function() { - me.open_document_list($(this).parent()); + me.open_document_list($(this).closest('.document-link')); }); // bind reports From 9025fce1c0308e3248190a7c375d1ed2d36ca6cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 01:03:13 +0530 Subject: [PATCH 010/515] fix(query): Use single quotes for string constant (backport #12948) (#12949) (cherry picked from commit 6225f9b35eaa760e817793c2f42f40a1038d720e) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 3565bbc32c..5be41f3568 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -115,7 +115,7 @@ def get_dict(fortype, name=None): messages.extend(get_server_messages(app)) messages = deduplicate_messages(messages) - messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""") + messages += frappe.db.sql("""select 'navbar', item_label from `tabNavbar Item` where item_label is not null""") messages = get_messages_from_include_files() messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`") messages += frappe.db.sql("select 'DocType:', name from tabDocType") From 226ad1d91aacef25cf8c1736fa872b78940713a2 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 17 Apr 2021 15:12:38 +0530 Subject: [PATCH 011/515] feat: New Build System based on esbuild - Deprecate use of build.json - *.bundle.js files placed anywhere in the public folder are bundled - Built files are created in public/build folder which is gitignored WIP --- .gitignore | 1 + esbuild/esbuild-plugin-html.js | 44 + esbuild/esbuild-watch.js | 69 ++ esbuild/ignore-assets.js | 11 + esbuild/index.js | 79 ++ esbuild/utils.js | 110 ++ frappe/hooks.py | 10 +- frappe/public/js/controls.bundle.js | 20 + frappe/public/js/desk.bundle.js | 107 ++ frappe/public/js/form.bundle.js | 18 + frappe/public/js/frappe-web.bundle.js | 25 + frappe/public/js/frappe/class.js | 2 +- frappe/public/js/frappe/form/layout.js | 2 +- frappe/public/js/frappe/utils/common.js | 3 +- frappe/public/js/frappe/utils/utils.js | 8 + .../js/frappe/views/reports/query_report.js | 2 +- frappe/public/js/libs.bundle.js | 11 + frappe/public/js/list.bundle.js | 42 + frappe/public/js/report.bundle.js | 9 + frappe/public/js/web/bootstrap-4.js | 65 + frappe/public/scss/common/datepicker.scss | 2 +- frappe/public/scss/common/quill.scss | 4 +- frappe/templates/base.html | 3 +- frappe/utils/jinja.py | 9 +- frappe/website/js/website.js | 3 +- frappe/www/app.html | 13 +- package.json | 14 +- yarn.lock | 1045 ++++++++++++++++- 28 files changed, 1676 insertions(+), 55 deletions(-) create mode 100644 esbuild/esbuild-plugin-html.js create mode 100644 esbuild/esbuild-watch.js create mode 100644 esbuild/ignore-assets.js create mode 100644 esbuild/index.js create mode 100644 esbuild/utils.js create mode 100644 frappe/public/js/controls.bundle.js create mode 100644 frappe/public/js/desk.bundle.js create mode 100644 frappe/public/js/form.bundle.js create mode 100644 frappe/public/js/frappe-web.bundle.js create mode 100644 frappe/public/js/libs.bundle.js create mode 100644 frappe/public/js/list.bundle.js create mode 100644 frappe/public/js/report.bundle.js create mode 100644 frappe/public/js/web/bootstrap-4.js diff --git a/.gitignore b/.gitignore index 766288fe2e..08d9876242 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ locale dist/ # build/ frappe/docs/current +frappe/public/build .vscode node_modules .kdev4/ diff --git a/esbuild/esbuild-plugin-html.js b/esbuild/esbuild-plugin-html.js new file mode 100644 index 0000000000..9edd6b627a --- /dev/null +++ b/esbuild/esbuild-plugin-html.js @@ -0,0 +1,44 @@ +module.exports = { + name: "frappe-html", + setup(build) { + let path = require("path"); + let fs = require("fs/promises"); + + build.onResolve({ filter: /\.html$/ }, args => { + return { + path: path.join(args.resolveDir, args.path), + namespace: "frappe-html" + }; + }); + + build.onLoad({ filter: /.*/, namespace: "frappe-html" }, args => { + let filepath = args.path; + let filename = path.basename(filepath).split(".")[0]; + + return fs + .readFile(filepath, "utf-8") + .then(content => { + content = scrub_html_template(content); + return { + contents: `\n\tfrappe.templates['${filename}'] = '${content}';\n` + }; + }) + .catch(() => { + return { + contents: "", + warnings: [ + { + text: `There was an error importing ${filepath}` + } + ] + }; + }); + }); + } +}; + +function scrub_html_template(content) { + content = content.replace(/\s/g, " "); + content = content.replace(/()/g, ""); + return content.replace("'", "'"); // eslint-disable-line +} diff --git a/esbuild/esbuild-watch.js b/esbuild/esbuild-watch.js new file mode 100644 index 0000000000..97d11024c2 --- /dev/null +++ b/esbuild/esbuild-watch.js @@ -0,0 +1,69 @@ +let esbuild = require("esbuild"); +let htmlPlugin = require("./esbuild-plugin-html"); +let vue = require("esbuild-vue"); +let http = require("http"); +let httpProxy = require("http-proxy"); +let path = require("path"); + +const { get_public_path, apps_list } = require("../rollup/rollup.utils"); + +let proxy = httpProxy.createProxyServer({}); + +proxy.on("error", function(e) { + console.error(e); +}); + +let server = http.createServer((req, res) => { + if (req.url.includes("/public/")) { + buildAsset(req.url).then(result => { + if (!result) { + proxy_request(); + return; + } + res.setHeader("Content-Header", "application/javascript"); + res.writeHead(200); + res.end(result); + }); + } else { + proxy_request(); + } + + function proxy_request() { + proxy.web(req, res, { target: "http://localhost:8000" }); + } +}); + +server.listen(8080); + +server.on("listening", () => { + console.log("dev server started at http:localhost:8080"); +}); + +function buildAsset(url) { + if (url.startsWith("/")) { + url = url.substr(1); + } + let app; + let parts = url.split(path.sep); + if (apps_list.includes(parts[0])) { + app = parts[0]; + } + if (!app) return; + + let filepath = path.resolve(get_public_path(app), url.split("public/")[1]); + console.log("building " + url); + + return esbuild + .build({ + entryPoints: [filepath], + write: false, + bundle: true, + plugins: [htmlPlugin, vue()], + define: { + "process.env.NODE_ENV": "'development'" + } + }) + .then(result => { + return result.outputFiles[0].text; + }); +} diff --git a/esbuild/ignore-assets.js b/esbuild/ignore-assets.js new file mode 100644 index 0000000000..5edfef2110 --- /dev/null +++ b/esbuild/ignore-assets.js @@ -0,0 +1,11 @@ +module.exports = { + name: "frappe-ignore-asset", + setup(build) { + build.onResolve({ filter: /^\/assets\// }, args => { + return { + path: args.path, + external: true + }; + }); + } +}; diff --git a/esbuild/index.js b/esbuild/index.js new file mode 100644 index 0000000000..419b6eb641 --- /dev/null +++ b/esbuild/index.js @@ -0,0 +1,79 @@ +let glob = require("fast-glob"); +let esbuild = require("esbuild"); +let html_plugin = require("./esbuild-plugin-html"); +let vue = require("esbuild-vue"); +let postCssPlugin = require("esbuild-plugin-postcss2").default; +let ignore_assets = require("./ignore-assets"); +let { get_options_for_scss } = require("../rollup/rollup.utils"); + +console.time("Build time"); + +glob(["frappe/public/js/**/*.bundle.js"]).then(entry_files => { + esbuild + .build({ + entryPoints: entry_files, + outdir: "frappe/public/build", + outbase: "frappe/public", + sourcemap: true, + bundle: true, + metafile: true, + minify: true, + define: { + "process.env.NODE_ENV": "'development'" + }, + plugins: [ + html_plugin, + ignore_assets, + vue(), + postCssPlugin({ + plugins: [require("autoprefixer")], + sassOptions: { + ...get_options_for_scss(), + importer: function(url) { + if (url.startsWith("~")) { + // strip ~ so that it can resolve from node_modules + url = url.slice(1); + } + if (url.endsWith(".css")) { + // strip .css from end of path + url = url.slice(0, -4); + } + // normal file, let it go + return { + file: url + }; + } + } + }) + ], + + // watch: { + // onRebuild(error, result) { + // if (error) console.error("watch build failed:", error); + // else { + // console.log("watch build succeeded:"); + // log_build_meta(result.metafile); + // } + // } + // } + }) + .then(result => { + log_build_meta(result.metafile); + + if (result.warnings.length) { + console.warn(result.warnings); + } + }) + .catch(e => console.error("error")) + .finally(() => { + console.timeEnd("Build time"); + }); +}); + +function log_build_meta(metafile) { + for (let outfile in metafile.outputs) { + if (outfile.endsWith('.map')) continue; + let data = metafile.outputs[outfile]; + console.log(outfile, data.bytes / 1000 + " Kb"); + } +} diff --git a/esbuild/utils.js b/esbuild/utils.js new file mode 100644 index 0000000000..6cecebcf59 --- /dev/null +++ b/esbuild/utils.js @@ -0,0 +1,110 @@ +const path = require("path"); +const fs = require("fs"); + +const frappe_path = path.resolve(__dirname, ".."); +const bench_path = path.resolve(frappe_path, "..", ".."); +const sites_path = path.resolve(bench_path, "sites"); +const assets_path = path.resolve(sites_path, "assets"); +const app_list = get_apps_list(); + +const app_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(bench_path, "apps", app, app); + return out; +}, {}); +const public_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(app_paths[app], "public"); + return out; +}, {}); +const public_js_paths = app_list.reduce((out, app) => { + out[app] = path.resolve(app_paths[app], "public/js"); + return out; +}, {}); + +const bundle_map = app_list.reduce((out, app) => { + const public_js_path = public_js_paths[app]; + if (fs.existsSync(public_js_path)) { + const all_files = fs.readdirSync(public_js_path); + const js_files = all_files.filter(file => file.endsWith(".js")); + + for (let js_file of js_files) { + const filename = path.basename(js_file).split(".")[0]; + out[path.join(app, "js", filename)] = path.resolve( + public_js_path, + js_file + ); + } + } + + return out; +}, {}); + +const get_public_path = app => public_paths[app]; + +const get_build_json_path = app => + path.resolve(get_public_path(app), "build.json"); + +function get_build_json(app) { + try { + return require(get_build_json_path(app)); + } catch (e) { + // build.json does not exist + return null; + } +} + +function delete_file(path) { + if (fs.existsSync(path)) { + fs.unlinkSync(path); + } +} + +function run_serially(tasks) { + let result = Promise.resolve(); + tasks.forEach(task => { + if (task) { + result = result.then ? result.then(task) : Promise.resolve(); + } + }); + return result; +} + +const get_app_path = app => app_paths[app]; + +const get_options_for_scss = () => { + const node_modules_path = path.resolve( + get_app_path("frappe"), + "..", + "node_modules" + ); + const app_paths = app_list + .map(get_app_path) + .map(app_path => path.resolve(app_path, "..")); + + return { + includePaths: [node_modules_path, ...app_paths] + }; +}; + +function get_apps_list() { + return fs + .readFileSync(path.resolve(sites_path, "apps.txt"), { + encoding: "utf-8" + }) + .split("\n") + .filter(Boolean); +} + +module.exports = { + app_list, + bench_path, + assets_path, + sites_path, + bundle_map, + get_public_path, + get_build_json_path, + get_build_json, + get_app_path, + delete_file, + run_serially, + get_options_for_scss +}; diff --git a/frappe/hooks.py b/frappe/hooks.py index 74c538c5df..967aed7f60 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -30,11 +30,11 @@ page_js = { # website app_include_js = [ "/assets/js/libs.min.js", - "/assets/js/desk.min.js", - "/assets/js/list.min.js", - "/assets/js/form.min.js", - "/assets/js/control.min.js", - "/assets/js/report.min.js", + "frappe/public/js/desk.bundle.js", + "frappe/public/js/list.bundle.js", + "frappe/public/js/form.bundle.js", + "frappe/public/js/controls.bundle.js", + "frappe/public/js/report.bundle.js", ] app_include_css = [ "/assets/css/desk.min.css", diff --git a/frappe/public/js/controls.bundle.js b/frappe/public/js/controls.bundle.js new file mode 100644 index 0000000000..f9a983ecae --- /dev/null +++ b/frappe/public/js/controls.bundle.js @@ -0,0 +1,20 @@ +import "air-datepicker/dist/js/datepicker.min.js"; +import "air-datepicker/dist/js/i18n/datepicker.cs.js"; +import "air-datepicker/dist/js/i18n/datepicker.da.js"; +import "air-datepicker/dist/js/i18n/datepicker.de.js"; +import "air-datepicker/dist/js/i18n/datepicker.en.js"; +import "air-datepicker/dist/js/i18n/datepicker.es.js"; +import "air-datepicker/dist/js/i18n/datepicker.fi.js"; +import "air-datepicker/dist/js/i18n/datepicker.fr.js"; +import "air-datepicker/dist/js/i18n/datepicker.hu.js"; +import "air-datepicker/dist/js/i18n/datepicker.nl.js"; +import "air-datepicker/dist/js/i18n/datepicker.pl.js"; +import "air-datepicker/dist/js/i18n/datepicker.pt-BR.js"; +import "air-datepicker/dist/js/i18n/datepicker.pt.js"; +import "air-datepicker/dist/js/i18n/datepicker.ro.js"; +import "air-datepicker/dist/js/i18n/datepicker.sk.js"; +import "air-datepicker/dist/js/i18n/datepicker.zh.js"; +import "./frappe/ui/capture.js"; +import "./frappe/form/controls/control.js"; + +console.log('controls') diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js new file mode 100644 index 0000000000..469714158c --- /dev/null +++ b/frappe/public/js/desk.bundle.js @@ -0,0 +1,107 @@ +import '../scss/desk.scss'; +import "./frappe/translate.js"; +import "./frappe/class.js"; +import "./frappe/polyfill.js"; +import "./frappe/provide.js"; +import "./frappe/assets.js"; +import "./frappe/format.js"; +import "./frappe/form/formatters.js"; +import "./frappe/dom.js"; +import "./frappe/ui/messages.js"; +import "./frappe/ui/keyboard.js"; +import "./frappe/ui/colors.js"; +import "./frappe/ui/sidebar.js"; +import "./frappe/ui/link_preview.js"; + +import "./frappe/request.js"; +import "./frappe/socketio_client.js"; +import "./frappe/utils/utils.js"; +import "./frappe/event_emitter.js"; +import "./frappe/router.js"; +import "./frappe/router_history.js"; +import "./frappe/defaults.js"; +import "./frappe/roles_editor.js"; +import "./frappe/module_editor.js"; +import "./frappe/microtemplate.js"; + +import "./frappe/ui/page.html"; +import "./frappe/ui/page.js"; +import "./frappe/ui/slides.js"; +// import "./frappe/ui/onboarding_dialog.js"; +import "./frappe/ui/find.js"; +import "./frappe/ui/iconbar.js"; +import "./frappe/form/layout.js"; +import "./frappe/ui/field_group.js"; +import "./frappe/form/link_selector.js"; +import "./frappe/form/multi_select_dialog.js"; +import "./frappe/ui/dialog.js"; +import "./frappe/ui/capture.js"; +import "./frappe/ui/app_icon.js"; +import "./frappe/ui/theme_switcher.js"; + +import "./frappe/model/model.js"; +import "./frappe/db.js"; +import "./frappe/model/meta.js"; +import "./frappe/model/sync.js"; +import "./frappe/model/create_new.js"; +import "./frappe/model/perm.js"; +import "./frappe/model/workflow.js"; +import "./frappe/model/user_settings.js"; + +import "./frappe/utils/user.js"; +import "./frappe/utils/common.js"; +import "./frappe/utils/urllib.js"; +import "./frappe/utils/pretty_date.js"; +import "./frappe/utils/test_utils.js"; +import "./frappe/utils/tools.js"; +import "./frappe/utils/datetime.js"; +import "./frappe/utils/number_format.js"; +import "./frappe/utils/help.js"; +import "./frappe/utils/help_links.js"; +import "./frappe/utils/address_and_contact.js"; +import "./frappe/utils/preview_email.js"; +import "./frappe/utils/file_manager.js"; + +import "./frappe/upload.js"; +import "./frappe/ui/tree.js"; + +import "./frappe/views/container.js"; +import "./frappe/views/breadcrumbs.js"; +import "./frappe/views/factory.js"; +import "./frappe/views/pageview.js"; + +import "./frappe/ui/toolbar/awesome_bar.js"; +// import "./frappe/ui/toolbar/energy_points_notifications.js"; +import "./frappe/ui/notifications/notifications.js"; +import "./frappe/ui/toolbar/search.js"; +import "./frappe/ui/toolbar/tag_utils.js"; +import "./frappe/ui/toolbar/search.html"; +import "./frappe/ui/toolbar/search_utils.js"; +import "./frappe/ui/toolbar/about.js"; +import "./frappe/ui/toolbar/navbar.html"; +import "./frappe/ui/toolbar/toolbar.js"; +// import "./frappe/ui/toolbar/notifications.js"; +import "./frappe/views/communication.js"; +import "./frappe/views/translation_manager.js"; +import "./frappe/views/workspace/workspace.js"; + +import "./frappe/widgets/widget_group.js"; + +import "./frappe/ui/sort_selector.html"; +import "./frappe/ui/sort_selector.js"; + +import "./frappe/change_log.html"; +import "./frappe/ui/workspace_loading_skeleton.html"; +import "./frappe/desk.js"; +import "./frappe/query_string.js"; + +// import "./frappe/ui/comment.js"; + +import "./frappe/chat.js"; +import "./frappe/utils/energy_point_utils.js"; +import "./frappe/utils/dashboard_utils.js"; +import "./frappe/ui/chart.js"; +import "./frappe/ui/datatable.js"; +import "./frappe/ui/driver.js"; +import "./frappe/ui/plyr.js"; +import "./frappe/barcode_scanner/index.js"; diff --git a/frappe/public/js/form.bundle.js b/frappe/public/js/form.bundle.js new file mode 100644 index 0000000000..994a5437c3 --- /dev/null +++ b/frappe/public/js/form.bundle.js @@ -0,0 +1,18 @@ +import "./frappe/form/templates/address_list.html"; +import "./frappe/form/templates/attachment.html"; +import "./frappe/form/templates/contact_list.html"; +import "./frappe/form/templates/form_dashboard.html"; +import "./frappe/form/templates/form_footer.html"; +import "./frappe/form/templates/form_links.html"; +import "./frappe/form/templates/form_sidebar.html"; +import "./frappe/form/templates/print_layout.html"; +import "./frappe/form/templates/report_links.html"; +import "./frappe/form/templates/set_sharing.html"; +import "./frappe/form/templates/timeline_message_box.html"; +import "./frappe/form/templates/users_in_sidebar.html"; + +import "./frappe/form/controls/control.js"; +import "./frappe/views/formview.js"; +import "./frappe/form/form.js"; +import "./frappe/meta_tag.js"; + diff --git a/frappe/public/js/frappe-web.bundle.js b/frappe/public/js/frappe-web.bundle.js new file mode 100644 index 0000000000..eef68b54c6 --- /dev/null +++ b/frappe/public/js/frappe-web.bundle.js @@ -0,0 +1,25 @@ +import "./frappe/class.js"; +import "./frappe/polyfill.js"; +import "./lib/md5.min.js"; +import "./frappe/provide.js"; +import "./frappe/format.js"; +import "./frappe/utils/number_format.js"; +import "./frappe/utils/utils.js"; +import "./frappe/utils/common.js"; +import "./frappe/ui/messages.js"; +import "./frappe/translate.js"; +import "./frappe/utils/pretty_date.js"; +import "./frappe/microtemplate.js"; +import "./frappe/query_string.js"; + +import "./frappe/upload.js"; + +import "./frappe/model/meta.js"; +import "./frappe/model/model.js"; +import "./frappe/model/perm.js"; + +import "./web/bootstrap-4"; + + +import "../../website/js/website.js"; +import "./frappe/socketio_client.js"; diff --git a/frappe/public/js/frappe/class.js b/frappe/public/js/frappe/class.js index 4f6dd0dc97..79ef2792ae 100644 --- a/frappe/public/js/frappe/class.js +++ b/frappe/public/js/frappe/class.js @@ -80,4 +80,4 @@ To subclass, use: // export global.Class = Class; - })(this); \ No newline at end of file + })(window); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 8b6c627882..d1437623a6 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -543,7 +543,7 @@ frappe.ui.form.Layout = Class.extend({ } else if (expression.substr(0, 5)=='eval:') { try { - out = eval(expression.substr(5)); + out = frappe.utils.eval(expression.substr(5), { doc }); if (parent && parent.istable && expression.includes('is_submittable')) { out = true; } diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 8fec3b2611..cce356f6ca 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -1,4 +1,5 @@ // common file between desk and website +import md5 from 'md5' frappe.avatar = function (user, css_class, title, image_url=null, remove_color=false, filterable=false) { let user_info; @@ -375,4 +376,4 @@ frappe.utils.get_page_view_count = function (route) { return frappe.call("frappe.website.doctype.web_page_view.web_page_view.get_page_view_count", { path: route }); -}; \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 7ce30a525c..fc3865a322 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -954,6 +954,14 @@ Object.assign(frappe.utils, { return $el; }, + eval(code, context={}) { + let variable_names = Object.keys(context); + let variables = Object.values(context); + code = `return (${code})`; + let expression_function = new Function(...variable_names, code); + return expression_function(...variables); + }, + get_browser() { let ua = navigator.userAgent; let tem; diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 834946b437..27aace1c75 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -445,7 +445,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { out = expression; } else if (expression.substr(0, 5) == 'eval:') { try { - out = eval(expression.substr(5)); + out = frappe.utils.eval(expression.substr(5), { doc }); } catch (e) { frappe.throw(__('Invalid "depends_on" expression set in filter {0}', [filter_label])); } diff --git a/frappe/public/js/libs.bundle.js b/frappe/public/js/libs.bundle.js new file mode 100644 index 0000000000..3d635e1f51 --- /dev/null +++ b/frappe/public/js/libs.bundle.js @@ -0,0 +1,11 @@ +import "bootstrap/dist/js/bootstrap.bundle.js"; +import Vue from "vue/dist/vue.esm.js"; +import moment from "moment/min/moment-with-locales.js"; +import momentTimezone from "moment-timezone/builds/moment-timezone-with-data.js"; +import "socket.io-client/dist/socket.io.slim.js"; +import "./lib/Sortable.min.js"; +import "./lib/jquery/jquery.hotkeys.js"; +import "./lib/jSignature.min.js"; + +window.moment = momentTimezone; +window.Vue = Vue; diff --git a/frappe/public/js/list.bundle.js b/frappe/public/js/list.bundle.js new file mode 100644 index 0000000000..e40ef94a81 --- /dev/null +++ b/frappe/public/js/list.bundle.js @@ -0,0 +1,42 @@ +import "./frappe/ui/listing.html"; + +import "./frappe/model/indicator.js"; +import "./frappe/ui/filters/filter.js"; +import "./frappe/ui/filters/filter_list.js"; +import "./frappe/ui/filters/field_select.js"; +import "./frappe/ui/filters/edit_filter.html"; +import "./frappe/ui/tags.js"; +import "./frappe/ui/tag_editor.js"; +import "./frappe/ui/like.js"; +// import "./frappe/ui/liked_by.html"; +import "../html/print_template.html"; + +import "./frappe/list/base_list.js"; +import "./frappe/list/list_view.js"; +import "./frappe/list/list_factory.js"; + +import "./frappe/list/list_view_select.js"; +import "./frappe/list/list_sidebar.js"; +import "./frappe/list/list_sidebar.html"; +import "./frappe/list/list_sidebar_stat.html"; +import "./frappe/list/list_sidebar_group_by.js"; +import "./frappe/list/list_view_permission_restrictions.html"; + +import "./frappe/views/gantt/gantt_view.js"; +import "./frappe/views/calendar/calendar.js"; +import "./frappe/views/dashboard/dashboard_view.js"; +import "./frappe/views/image/image_view.js"; +import "./frappe/views/map/map_view.js"; +import "./frappe/views/kanban/kanban_view.js"; +import "./frappe/views/inbox/inbox_view.js"; +import "./frappe/views/file/file_view.js"; + +import "./frappe/views/treeview.js"; +import "./frappe/views/interaction.js"; + +import "./frappe/views/image/image_view_item_row.html"; +import "./frappe/views/image/photoswipe_dom.html"; + +import "./frappe/views/kanban/kanban_board.html"; +import "./frappe/views/kanban/kanban_column.html"; +import "./frappe/views/kanban/kanban_card.html"; diff --git a/frappe/public/js/report.bundle.js b/frappe/public/js/report.bundle.js new file mode 100644 index 0000000000..3f2aa3b4c1 --- /dev/null +++ b/frappe/public/js/report.bundle.js @@ -0,0 +1,9 @@ +import "./lib/clusterize.min.js"; +import "./frappe/views/reports/report_factory.js"; +import "./frappe/views/reports/report_view.js"; +import "./frappe/views/reports/query_report.js"; +import "./frappe/views/reports/print_grid.html"; +import "./frappe/views/reports/print_tree.html"; +import "./frappe/ui/group_by/group_by.html"; +import "./frappe/ui/group_by/group_by.js"; +import "./frappe/views/reports/report_utils.js"; diff --git a/frappe/public/js/web/bootstrap-4.js b/frappe/public/js/web/bootstrap-4.js new file mode 100644 index 0000000000..c12919c570 --- /dev/null +++ b/frappe/public/js/web/bootstrap-4.js @@ -0,0 +1,65 @@ +import 'bootstrap/dist/js/bootstrap.bundle'; + +// multilevel dropdown +$('.dropdown-menu a.dropdown-toggle').on('click', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + if (!$(this).next().hasClass('show')) { + $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); + } + var $subMenu = $(this).next(".dropdown-menu"); + $subMenu.toggleClass('show'); + + + $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function () { + $('.dropdown-submenu .show').removeClass("show"); + }); + + return false; +}); + +frappe.get_modal = function (title, content) { + return $( + `` + ); +}; + +frappe.ui.Dialog = class Dialog extends frappe.ui.Dialog { + get_primary_btn() { + return this.$wrapper.find(".modal-footer .btn-primary"); + } + + set_primary_action(label, click) { + this.$wrapper.find('.modal-footer').removeClass('hidden'); + return super.set_primary_action(label, click) + .removeClass('hidden'); + } + + make() { + super.make(); + if (this.fields) { + this.$wrapper.find('.section-body').addClass('w-100'); + } + } +}; diff --git a/frappe/public/scss/common/datepicker.scss b/frappe/public/scss/common/datepicker.scss index d159f27fc1..93bdfcc03d 100644 --- a/frappe/public/scss/common/datepicker.scss +++ b/frappe/public/scss/common/datepicker.scss @@ -1,4 +1,4 @@ -@import "~air-datepicker/dist/css/datepicker.min.css"; +@import "~air-datepicker/dist/css/datepicker.min"; .datepicker { diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index d15ca7e036..95942d7c61 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -1,5 +1,5 @@ -@import '~quill/dist/quill.snow.css'; -@import '~quill/dist/quill.bubble.css'; +@import '~quill/dist/quill.snow'; +@import '~quill/dist/quill.bubble'; .ql-toolbar.ql-snow, .ql-container.ql-snow { diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 78aa573c99..7eb4e5e8d6 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -97,8 +97,7 @@ {% block base_scripts %} - - + {% endblock %} {%- for link in web_include_js %} diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index cd74b2a283..8a80013887 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -23,7 +23,8 @@ def get_jenv(): 'resolve_class': resolve_class, 'inspect': inspect, 'web_blocks': web_blocks, - 'web_block': web_block + 'web_block': web_block, + 'js_asset': js_asset }) frappe.local.jenv = jenv @@ -228,3 +229,9 @@ def web_blocks(blocks): html += ''.format(script) return html + +def js_asset(path): + import frappe + if not frappe.local.dev_server or True: + path = path.replace('frappe/public/', '/assets/frappe/build/') + return f'' diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index ea0b9aedfa..9e800c8f1a 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -605,7 +605,8 @@ $(document).ready(function() { $(document).on("page-change", function() { $(document).trigger("apply_permissions"); - $('.dropdown-toggle').dropdown(); + // TODO: esbuild + // $('.dropdown-toggle').dropdown(); //multilevel dropdown fix $('.dropdown-menu .dropdown-submenu .dropdown-toggle').on('click', function(e) { diff --git a/frappe/www/app.html b/frappe/www/app.html index 8da4d11c00..419d3ffe07 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -52,11 +52,14 @@ - {% for include in include_js %} - - {% endfor %} - {% include "templates/includes/app_analytics/google_analytics.html" %} - {% include "templates/includes/app_analytics/mixpanel_analytics.html" %} + {% for include in include_js %} + {{ js_asset(include) }} + {% endfor %} + + {{ js_asset('frappe/public/js/test.bundle.js') }} + + {% include "templates/includes/app_analytics/google_analytics.html" %} + {% include "templates/includes/app_analytics/mixpanel_analytics.html" %} {% for sound in (sounds or []) %}