diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 8e503cce46..053d015366 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -13,7 +13,7 @@ context('Awesome Bar', () => { it('navigates to doctype list', () => { cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 700 }); cy.get('.awesomplete').findByRole('listbox').should('be.visible'); - cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 }); + cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{enter}', { delay: 700 }); cy.get('.title-text').should('contain', 'To Do'); @@ -22,7 +22,7 @@ context('Awesome Bar', () => { it('find text in doctype list', () => { cy.findByPlaceholderText('Search or type a command (Ctrl + G)') - .type('test in todo{downarrow}{enter}', { delay: 700 }); + .type('test in todo{enter}', { delay: 700 }); cy.get('.title-text').should('contain', 'To Do'); @@ -32,7 +32,7 @@ context('Awesome Bar', () => { it('navigates to new form', () => { cy.findByPlaceholderText('Search or type a command (Ctrl + G)') - .type('new blog post{downarrow}{enter}', { delay: 700 }); + .type('new blog post{enter}', { delay: 700 }); cy.get('.title-text:visible').should('have.text', 'New Blog Post'); }); diff --git a/cypress/integration/control_attach.js b/cypress/integration/control_attach.js new file mode 100644 index 0000000000..0552780737 --- /dev/null +++ b/cypress/integration/control_attach.js @@ -0,0 +1,90 @@ +context('Attach Control', () => { + before(() => { + cy.login(); + cy.visit('/app/doctype'); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { + name: 'Test Attach Control', + fields: [ + { + "label": "Attach File or Image", + "fieldname": "attach", + "fieldtype": "Attach", + "in_list_view": 1, + }, + ] + }); + }); + }); + it('Checking functionality for "Link" button in the "Attach" fieldtype', () => { + //Navigating to the new form for the newly created doctype + cy.new_form('Test Attach Control'); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole('button', {name: 'Attach'}).click(); + + //Clicking on "Link" button to attach a file using the "Link" button + cy.findByRole('button', {name: 'Link'}).click(); + cy.findByPlaceholderText('Attach a web link').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500}); + cy.wait("@upload_image"); + cy.findByRole('button', {name: 'Save'}).click(); + + //Checking if the URL of the attached image is getting displayed in the field of the newly created doctype + cy.get('.attached-file > .ellipsis > .attached-file-link') + .should('have.attr', 'href') + .and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); + + //Clicking on the "Clear" button + cy.get('[data-action="clear_attachment"]').click(); + + //Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button + cy.get('.control-input > .btn-sm').should('contain', 'Attach'); + + //Deleting the doc + cy.go_to_list('Test Attach Control'); + cy.get('.list-row-checkbox').eq(0).click(); + cy.get('.actions-btn-group > .btn').contains('Actions').click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button('Yes'); + }); + + it('Checking functionality for "Library" button in the "Attach" fieldtype', () => { + //Navigating to the new form for the newly created doctype + cy.new_form('Test Attach Control'); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole('button', {name: 'Attach'}).click(); + + //Clicking on "Library" button to attach a file using the "Library" button + cy.findByRole('button', {name: 'Library'}).click(); + cy.contains('72402.jpg').click(); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500}); + cy.wait("@upload_image"); + cy.findByRole('button', {name: 'Save'}).click(); + + //Checking if the URL of the attached image is getting displayed in the field of the newly created doctype + cy.get('.attached-file > .ellipsis > .attached-file-link') + .should('have.attr', 'href') + .and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); + + //Clicking on the "Clear" button + cy.get('[data-action="clear_attachment"]').click(); + + //Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button + cy.get('.control-input > .btn-sm').should('contain', 'Attach'); + + //Deleting the doc + cy.go_to_list('Test Attach Control'); + cy.get('.list-row-checkbox').eq(0).click(); + cy.get('.actions-btn-group > .btn').contains('Actions').click(); + cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); + cy.click_modal_primary_button('Yes'); + }); +}); \ No newline at end of file diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js new file mode 100644 index 0000000000..35c585306c --- /dev/null +++ b/cypress/integration/control_date.js @@ -0,0 +1,71 @@ +context('Date Control', () => { + before(() => { + cy.login(); + cy.visit('/app/doctype'); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { + name: 'Test Date Control', + fields: [ + { + "label": "Date", + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1 + }, + ] + }); + }); + }); + it('Selecting a date from the datepicker', () => { + cy.new_form('Test Date Control'); + cy.get_field('date', 'Date').click(); + cy.get('.datepicker--nav-title').click(); + cy.get('.datepicker--nav-title').click({force: true}); + + + //Inputing values in the date field + cy.get('.datepicker--years > .datepicker--cells > .datepicker--cell[data-year=2020]').click(); + cy.get('.datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]').click(); + cy.get('.datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]').click(); + + //Verifying if the selected date is displayed in the date field + cy.get_field('date', 'Date').should('have.value', '01-15-2020'); + }); + + it('Checking next and previous button', () => { + cy.get_field('date', 'Date').click(); + + //Clicking on the next button in the datepicker + cy.get('.datepicker--nav-action[data-action=next]').click(); + + //Selecting a date from the datepicker + cy.get('.datepicker--cell[data-date=15]').click({force: true}); + + //Verifying if the selected date has been displayed in the date field + cy.get_field('date', 'Date').should('have.value', '02-15-2020'); + cy.wait(500); + cy.get_field('date', 'Date').click(); + + //Clicking on the previous button in the datepicker + cy.get('.datepicker--nav-action[data-action=prev]').click(); + + //Selecting a date from the datepicker + cy.get('.datepicker--cell[data-date=15]').click({force: true}); + + //Verifying if the selected date has been displayed in the date field + cy.get_field('date', 'Date').should('have.value', '01-15-2020'); + }); + + it('Clicking on "Today" button gives todays date', () => { + cy.get_field('date', 'Date').click(); + + //Clicking on "Today" button + cy.get('.datepicker--button').click(); + + //Picking up the todays date + const todays_date = Cypress.moment().format('MM-DD-YYYY'); + + //Verifying if clicking on "Today" button matches today's date + cy.get_field('date', 'Date').should('have.value', todays_date); + }); +}); \ No newline at end of file diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 520c0008c5..548d21bb60 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -49,7 +49,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -109,10 +109,11 @@ "link_fieldname": "server_script" } ], - "modified": "2021-09-04 12:02:43.671240", + "modified": "2022-04-07 19:41:23.178772", "modified_by": "Administrator", "module": "Core", "name": "Server Script", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -130,5 +131,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json index 657e9df89d..c92b2005ed 100644 --- a/frappe/desk/doctype/system_console/system_console.json +++ b/frappe/desk/doctype/system_console/system_console.json @@ -1,7 +1,7 @@ { "actions": [ { - "action": "#List/Console Log/List", + "action": "app/console-log", "action_type": "Route", "label": "Logs" }, @@ -86,7 +86,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-15 17:17:44.844767", + "modified": "2022-04-09 16:35:32.345542", "modified_by": "Administrator", "module": "Desk", "name": "System Console", @@ -104,5 +104,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/installer.py b/frappe/installer.py index ffb595e9ad..c7dacc4ac1 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -222,7 +222,7 @@ def install_app(name, verbose=False, set_as_patched=True): # install pre-requisites if app_hooks.required_apps: for app in app_hooks.required_apps: - name = parse_app_name(name) + name = parse_app_name(app) install_app(name, verbose=verbose) frappe.flags.in_install = name diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 3e9d1317e8..57591d01d5 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -132,32 +132,30 @@ class BaseDocument(object): def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) - def get(self, key=None, filters=None, limit=None, default=None): - if key: - if isinstance(key, dict): - return _filter(self.get_all_children(), key, limit=limit) - if filters: - if isinstance(filters, dict): - value = _filter(self.__dict__.get(key, []), filters, limit=limit) - else: - default = filters - filters = None - value = self.__dict__.get(key, default) + def get(self, key, filters=None, limit=None, default=None): + if isinstance(key, dict): + return _filter(self.get_all_children(), key, limit=limit) + + if filters: + if isinstance(filters, dict): + value = _filter(self.__dict__.get(key, []), filters, limit=limit) else: + default = filters + filters = None value = self.__dict__.get(key, default) - - if value is None and key in ( - d.fieldname for d in self.meta.get_table_fields() - ): - value = [] - self.set(key, value) - - if limit and isinstance(value, (list, tuple)) and len(value) > limit: - value = value[:limit] - - return value else: - return self.__dict__ + value = self.__dict__.get(key, default) + + if value is None and key in ( + d.fieldname for d in self.meta.get_table_fields() + ): + value = [] + self.set(key, value) + + if limit and isinstance(value, (list, tuple)) and len(value) > limit: + value = value[:limit] + + return value def getone(self, key, filters=None): return self.get(key, filters=filters, limit=1)[0] diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 16056d382a..b573e1d301 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -476,7 +476,7 @@ class DatabaseQuery(object): if 'ifnull(' in f.fieldname: column_name = self.cast_name(f.fieldname, "ifnull(") else: - column_name = self.cast_name(f"{tname}.{f.fieldname}") + column_name = self.cast_name(f"{tname}.`{f.fieldname}`") if f.operator.lower() in additional_filters_config: f.update(get_additional_filter_field(additional_filters_config, f, f.value)) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 4768faff48..0383327b68 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -45,7 +45,7 @@ def export_customizations(module, doctype, sync_on_migrate=0, with_permissions=0 if not frappe.get_conf().developer_mode: raise Exception('Not developer mode') - custom = {'custom_fields': [], 'property_setters': [], 'custom_perms': [], + custom = {'custom_fields': [], 'property_setters': [], 'custom_perms': [],'links':[], 'doctype': doctype, 'sync_on_migrate': sync_on_migrate} def add(_doctype): @@ -53,6 +53,8 @@ def export_customizations(module, doctype, sync_on_migrate=0, with_permissions=0 fields='*', filters={'dt': _doctype}) custom['property_setters'] += frappe.get_all('Property Setter', fields='*', filters={'doc_type': _doctype}) + custom['links'] += frappe.get_all('DocType Link', + fields='*', filters={'parent': _doctype}) add(doctype) diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 4ee52d16b8..e22235f60f 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -44,6 +44,8 @@ frappe.ui.form.Control = class BaseControl { } if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { + let status = "Write"; + // like in case of a dialog box if (cint(this.df.hidden)) { // eslint-disable-next-line @@ -55,10 +57,10 @@ frappe.ui.form.Control = class BaseControl { if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console return "None"; - } else if (cint(this.df.read_only || this.df.is_virtual)) { + } else if (cint(this.df.read_only || this.df.is_virtual || this.df.fieldtype === "Read Only")) { // eslint-disable-next-line if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console - return "Read"; + status = "Read"; } else if ((this.grid && this.grid.display_status == 'Read') || @@ -67,10 +69,16 @@ frappe.ui.form.Control = class BaseControl { this.layout.grid.display_status == 'Read')) { // parent grid is read if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console - return "Read"; + status = "Read"; } - return "Write"; + if ( + status === "Read" && + is_null(this.value) && + !in_list(["HTML", "Image", "Button"], this.df.fieldtype) + ) status = "None"; + + return status; } var status = frappe.perm.get_field_display_status(this.df, diff --git a/frappe/public/js/frappe/form/controls/control.js b/frappe/public/js/frappe/form/controls/control.js index bd04938e35..90e8697b1c 100644 --- a/frappe/public/js/frappe/form/controls/control.js +++ b/frappe/public/js/frappe/form/controls/control.js @@ -23,7 +23,6 @@ import './table'; import './color'; import './signature'; import './password'; -import './read_only'; import './button'; import './html'; import './markdown_editor'; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index f4c9849528..95abba616a 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -262,3 +262,5 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp return this.grid || this.layout && this.layout.grid; } }; + +frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData; diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 280eac3941..688e7da3e0 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -58,7 +58,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f })); this.add_non_group_layers(data_layers, this.editableLayers); try { - this.map.flyToBounds(this.editableLayers.getBounds(), { + this.map.fitBounds(this.editableLayers.getBounds(), { padding: [50,50] }); } @@ -66,10 +66,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f // suppress error if layer has a point. } this.editableLayers.addTo(this.map); - this.map._onResize(); - } else if ((value===undefined) || (value == JSON.stringify(new L.FeatureGroup().toGeoJSON()))) { - this.locate_control.start(); + } else { + this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom); } + this.map.invalidateSize(); } bind_leaflet_map() { @@ -97,8 +97,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f }); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center, - frappe.utils.map_defaults.zoom); + this.map = L.map(this.map_id); L.tileLayer(frappe.utils.map_defaults.tiles, frappe.utils.map_defaults.options).addTo(this.map); @@ -146,9 +145,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f }; // create control and add to map - var drawControl = new L.Control.Draw(options); - - this.map.addControl(drawControl); + this.drawControl = new L.Control.Draw(options); + this.map.addControl(this.drawControl); this.map.on('draw:created', (e) => { var type = e.layerType, diff --git a/frappe/public/js/frappe/form/controls/read_only.js b/frappe/public/js/frappe/form/controls/read_only.js deleted file mode 100644 index 2f1d1a2bca..0000000000 --- a/frappe/public/js/frappe/form/controls/read_only.js +++ /dev/null @@ -1,8 +0,0 @@ -frappe.ui.form.ControlReadOnly = class ControlReadOnly extends frappe.ui.form.ControlData { - get_status(explain) { - var status = super.get_status(explain); - if(status==="Write") - status = "Read"; - return; - } -}; diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 0eabfdd337..3ea9c6bc95 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -225,7 +225,10 @@ $.extend(frappe.perm, { if (explain) console.log("By Workflow:" + status); // read only field is checked - if (status === "Write" && cint(df.read_only)) { + if (status === "Write" && ( + cint(df.read_only) || + df.fieldtype === "Read Only" + )) { status = "Read"; } if (explain) console.log("By Read Only:" + status); @@ -276,4 +279,4 @@ $.extend(frappe.perm, { return allowed_docs; } } -}); \ No newline at end of file +}); diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 178d1a65cb..eb3dcc4f89 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -22,17 +22,15 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { super.make(); this.refresh(); // set default - $.each(this.fields_list, function(i, field) { - if (field.df["default"]) { - let def_value = field.df["default"]; + $.each(this.fields_list, (_, field) => { + if (!is_null(field.df.default)) { + let def_value = field.df.default; - if (def_value == 'Today' && field.df["fieldtype"] == 'Date') { + if (def_value === "Today" && field.df.fieldtype === "Date") { def_value = frappe.datetime.get_today(); } - field.set_input(def_value); - // if default and has depends_on, render its fields. - me.refresh_dependency(); + this.set_value(field.df.fieldname, def_value); } }) @@ -129,6 +127,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { if (f) { f.set_value(val).then(() => { f.set_input(val); + f.refresh(); this.refresh_dependency(); resolve(); }); diff --git a/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js new file mode 100644 index 0000000000..59fccbccc5 --- /dev/null +++ b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js @@ -0,0 +1,191 @@ +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// VERSION +// 0.1.0 (2016-03-28) Initial release +// +// AUTHOR +// Forrest Smith +// +// CONTRIBUTORS +// J�rgen Tjern� - async helper +// Anurag Awasthi - updated to 0.2.0 + +const SEQUENTIAL_BONUS = 15; // bonus for adjacent matches +const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator +const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower +const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched + +const LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in str before the first match +const MAX_LEADING_LETTER_PENALTY = -15; // maximum penalty for leading letters +const UNMATCHED_LETTER_PENALTY = -1; + + +/** + * Does a fuzzy search to find pattern inside a string. + * @param {*} pattern string pattern to search for + * @param {*} str string string which is being searched + * @returns [boolean, number] a boolean which tells if pattern was + * found or not and a search score + */ +export function fuzzy_match(pattern, str) { + const recursion_count = 0; + const recursion_limit = 10; + const matches = []; + const max_matches = 256; + + return fuzzy_match_recursive( + pattern, + str, + 0 /* pattern_cur_index */, + 0 /* str_curr_index */, + null /* src_matches */, + matches, + max_matches, + 0 /* next_match */, + recursion_count, + recursion_limit + ); +} + +function fuzzy_match_recursive( + pattern, + str, + pattern_cur_index, + str_curr_index, + src_matches, + matches, + max_matches, + next_match, + recursion_count, + recursion_limit +) { + let out_score = 0; + + // Return if recursion limit is reached. + if (++recursion_count >= recursion_limit) { + return [false, out_score]; + } + + // Return if we reached ends of strings. + if (pattern_cur_index === pattern.length || str_curr_index === str.length) { + return [false, out_score]; + } + + // Recursion params + let recursive_match = false; + let best_recursive_matches = []; + let best_recursive_score = 0; + + // Loop through pattern and str looking for a match. + let first_match = true; + while (pattern_cur_index < pattern.length && str_curr_index < str.length) { + // Match found. + if ( + pattern[pattern_cur_index].toLowerCase() === str[str_curr_index].toLowerCase() + ) { + if (next_match >= max_matches) { + return [false, out_score]; + } + + if (first_match && src_matches) { + matches = [...src_matches]; + first_match = false; + } + + const recursive_matches = []; + const [matched, recursive_score] = fuzzy_match_recursive( + pattern, + str, + pattern_cur_index, + str_curr_index + 1, + matches, + recursive_matches, + max_matches, + next_match, + recursion_count, + recursion_limit + ); + + if (matched) { + // Pick best recursive score. + if (!recursive_match || recursive_score > best_recursive_score) { + best_recursive_matches = [...recursive_matches]; + best_recursive_score = recursive_score; + } + recursive_match = true; + } + + matches[next_match++] = str_curr_index; + ++pattern_cur_index; + } + ++str_curr_index; + } + + const matched = pattern_cur_index === pattern.length; + + if (matched) { + out_score = 100; + + // Apply leading letter penalty + let penalty = LEADING_LETTER_PENALTY * matches[0]; + penalty = + penalty < MAX_LEADING_LETTER_PENALTY + ? MAX_LEADING_LETTER_PENALTY + : penalty; + out_score += penalty; + + //Apply unmatched penalty + const unmatched = str.length - next_match; + out_score += UNMATCHED_LETTER_PENALTY * unmatched; + + // Apply ordering bonuses + for (let i = 0; i < next_match; i++) { + const curr_idx = matches[i]; + + if (i > 0) { + const prev_idx = matches[i - 1]; + if (curr_idx == prev_idx + 1) { + out_score += SEQUENTIAL_BONUS; + } + } + + // Check for bonuses based on neighbor character value. + if (curr_idx > 0) { + // Camel case + const neighbor = str[curr_idx - 1]; + const curr = str[curr_idx]; + if ( + neighbor !== neighbor.toUpperCase() && + curr !== curr.toLowerCase() + ) { + out_score += CAMEL_BONUS; + } + const is_neighbour_separator = neighbor == "_" || neighbor == " "; + if (is_neighbour_separator) { + out_score += SEPARATOR_BONUS; + } + } else { + // First letter + out_score += FIRST_LETTER_BONUS; + } + } + + // Return best result + if (recursive_match && (!matched || best_recursive_score > out_score)) { + // Recursive score is better than "this" + matches = [...best_recursive_matches]; + out_score = best_recursive_score; + return [true, out_score]; + } else if (matched) { + // "this" score is better than recursive + return [true, out_score]; + } else { + return [false, out_score]; + } + } + return [false, out_score]; +} diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js index 9700276568..1571522489 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_utils.js +++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js @@ -1,4 +1,6 @@ frappe.provide('frappe.search'); +import { fuzzy_match } from './fuzzy_match.js'; + frappe.search.utils = { setup_recent: function() { @@ -533,101 +535,46 @@ frappe.search.utils = { }, fuzzy_search: function(keywords, _item) { - // Returns 10 for case-perfect contain, 0 for not found - // 9 for perfect contain, - // 0 - 6 for fuzzy contain - - // **Specific use-case step** keywords = keywords || ''; var item = __(_item || ''); - var item_without_hyphen = item.replace(/-/g, " "); - - var item_length = item.length; - var query_length = keywords.length; - var length_ratio = query_length / item_length; - var max_skips = 3, max_mismatch_len = 2; - - if (query_length > item_length) { - return 0; - } - - // check for perfect string matches or - // matches that start with the keyword - if ([item, item_without_hyphen].includes(keywords) - || [item, item_without_hyphen].some((txt) => txt.toLowerCase().indexOf(keywords) === 0)) { - return 10 + length_ratio; - } - - if (item.indexOf(keywords) !== -1 && keywords !== keywords.toLowerCase()) { - return 9 + length_ratio; - } - - item = item.toLowerCase(); - keywords = keywords.toLowerCase(); - - if (item.indexOf(keywords) !== -1) { - return 8 + length_ratio; - } - - var skips = 0, mismatches = 0; - outer: for (var i = 0, j = 0; i < query_length; i++) { - if (mismatches !== 0) skips++; - if (skips > max_skips) return 0; - var k_ch = keywords.charCodeAt(i); - mismatches = 0; - while (j < item_length) { - if (item.charCodeAt(j++) === k_ch) { - continue outer; - } - if(++mismatches > max_mismatch_len) return 0 ; - } - return 0; - } - - // Since indexOf didn't pass, there will be atleast 1 skip - // hence no divide by zero, but just to be safe - if((skips + mismatches) > 0) { - return (5 + length_ratio)/(skips + mismatches); - } else { - return 0; - } + var match = fuzzy_match(keywords, item); + return match[1]; }, bolden_match_part: function(str, subseq) { - var rendered = ""; - if(this.fuzzy_search(subseq, str) === 0) { + if (fuzzy_match(subseq, str)[0] === false) { return str; - } else if(this.fuzzy_search(subseq, str) > 6) { - var regEx = new RegExp("("+ subseq +")", "ig"); - return str.replace(regEx, '$1'); - } else { - var str_orig = str; - var str = str.toLowerCase(); - var str_len = str.length; - var subseq = subseq.toLowerCase(); - - outer: for(var i = 0, j = 0; i < subseq.length; i++) { - var sub_ch = subseq.charCodeAt(i); - while(j < str_len) { - if(str.charCodeAt(j) === sub_ch) { - var str_char = str_orig.charAt(j); - if(str_char === str_char.toLowerCase()) { - rendered += '' + subseq.charAt(i) + ''; - } else { - rendered += '' + subseq.charAt(i).toUpperCase() + ''; - } - j++; - continue outer; - } - rendered += str_orig.charAt(j); - j++; - } - return str_orig; - } - rendered += str_orig.slice(j); - return rendered; } + if (str.indexOf(subseq) == 0) { + var tail = str.split(subseq)[1]; + return '' + subseq + '' + tail; + } + var rendered = ""; + var str_orig = str; + var str_len = str.length; + str = str.toLowerCase(); + subseq = subseq.toLowerCase(); + outer: for (var i = 0, j = 0; i < subseq.length; i++) { + var sub_ch = subseq.charCodeAt(i); + while (j < str_len) { + if (str.charCodeAt(j) === sub_ch) { + var str_char = str_orig.charAt(j); + if (str_char === str_char.toLowerCase()) { + rendered += '' + subseq.charAt(i) + ''; + } else { + rendered += '' + subseq.charAt(i).toUpperCase() + ''; + } + j++; + continue outer; + } + rendered += str_orig.charAt(j); + j++; + } + return str_orig; + } + rendered += str_orig.slice(j); + return rendered; }, get_executables(keywords) { diff --git a/frappe/public/scss/desk/avatar.scss b/frappe/public/scss/desk/avatar.scss index 638256c21d..073f90e20f 100644 --- a/frappe/public/scss/desk/avatar.scss +++ b/frappe/public/scss/desk/avatar.scss @@ -73,6 +73,7 @@ display: inline-block; width: 100%; height: 100%; + object-fit: cover; background-color: var(--avatar-frame-bg); background-size: cover; background-repeat: no-repeat; @@ -145,6 +146,7 @@ .standard-image { width: 100%; height: 100%; + object-fit: cover; display: flex; justify-content: center; align-items: center; diff --git a/frappe/public/scss/desk/page.scss b/frappe/public/scss/desk/page.scss index 5206e2919c..b0a24eed38 100644 --- a/frappe/public/scss/desk/page.scss +++ b/frappe/public/scss/desk/page.scss @@ -51,11 +51,6 @@ } } -.custom-actions { - display: flex; - align-items: center; -} - .page-actions { align-items: center; .btn { @@ -72,6 +67,11 @@ .custom-btn-group { display: inline-flex; } + + .custom-actions { + display: flex; + align-items: center; + } } .layout-main-section-wrapper { diff --git a/frappe/public/scss/website/base.scss b/frappe/public/scss/website/base.scss index 7e5d9b5b66..d666bcd410 100644 --- a/frappe/public/scss/website/base.scss +++ b/frappe/public/scss/website/base.scss @@ -1,13 +1,3 @@ -$font-size-xs: 0.7rem; -$font-size-sm: 0.85rem; -$font-size-lg: 1.12rem; -$font-size-xl: 1.25rem; -$font-size-2xl: 1.5rem; -$font-size-3xl: 2rem; -$font-size-4xl: 2.5rem; -$font-size-5xl: 3rem; -$font-size-6xl: 4rem; - html { height: 100%; } @@ -29,68 +19,67 @@ h1, h2, h3, h4 { } h1 { - font-size: $font-size-3xl; + font-size: 2rem; line-height: 1.25; letter-spacing: -0.025em; margin-top: 3rem; margin-bottom: 0.75rem; @include media-breakpoint-up(sm) { - font-size: $font-size-5xl; - line-height: 2.5rem; + font-size: 2.5rem; margin-top: 3.5rem; margin-bottom: 1.25rem; } @include media-breakpoint-up(xl) { - font-size: $font-size-6xl; + font-size: 3.5rem; line-height: 1; margin-top: 4rem; } } h2 { - font-size: $font-size-2xl; + font-size: 1.4rem; margin-top: 2rem; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; @include media-breakpoint-up(sm) { - font-size: $font-size-3xl; + font-size: 2rem; margin-top: 4rem; - margin-bottom: 1rem; + margin-bottom: 0.75rem; } @include media-breakpoint-up(xl) { - font-size: $font-size-4xl; + font-size: 2.5rem; margin-top: 4rem; } } h3 { - font-size: $font-size-xl; - margin-top: 1.5rem; + font-size: 1.2rem; + margin-top: 2rem; margin-bottom: 0.5rem; @include media-breakpoint-up(sm) { - font-size: $font-size-2xl; + font-size: 1.4rem; margin-top: 2.5rem; } @include media-breakpoint-up(xl) { - font-size: $font-size-3xl; + font-size: 1.9rem; margin-top: 3.5rem; } } h4 { - font-size: $font-size-lg; - margin-top: 1rem; + font-size: 1.1rem; + margin-top: 2rem; margin-bottom: 0.5rem; @include media-breakpoint-up(sm) { - font-size: $font-size-xl; - margin-top: 1.25rem; + font-size: 1.3rem; + margin-top: 2.5rem; } @include media-breakpoint-up(xl) { - font-size: $font-size-2xl; - margin-top: 1.75rem; + font-size: 1.5rem; + margin-top: 3rem; } a { @@ -98,6 +87,10 @@ h4 { } } -.btn.btn-lg { - font-size: $font-size-lg; +p { + line-height: 1.7; +} + +.btn.btn-lg { + font-size: 1.1rem; } diff --git a/frappe/public/scss/website/blog.scss b/frappe/public/scss/website/blog.scss index 4f289db125..ebc147b238 100644 --- a/frappe/public/scss/website/blog.scss +++ b/frappe/public/scss/website/blog.scss @@ -14,6 +14,10 @@ } } +.blog-list-content { + margin-bottom: 3rem; +} + .blog-card { margin-bottom: 2rem; position: relative; @@ -98,10 +102,15 @@ .blog-header { margin-bottom: 3rem; - margin-top: 3rem; + margin-top: 5rem; } } + .blog-comments { + margin-top: 1rem; + margin-bottom: 5rem; + } + .feedback-item svg { vertical-align: sub; diff --git a/frappe/public/scss/website/error-state.scss b/frappe/public/scss/website/error-state.scss index c869e9e1df..7a83fc0084 100644 --- a/frappe/public/scss/website/error-state.scss +++ b/frappe/public/scss/website/error-state.scss @@ -1,4 +1,5 @@ .error-page { + margin: 3rem 0; text-align: center; .img-404 { diff --git a/frappe/public/scss/website/footer.scss b/frappe/public/scss/website/footer.scss index e5dae72808..9a36d7ab6d 100644 --- a/frappe/public/scss/website/footer.scss +++ b/frappe/public/scss/website/footer.scss @@ -1,5 +1,5 @@ .web-footer { - margin: 5rem 0; + padding: 3rem 0; min-height: 140px; background-color: var(--fg-color); border-top: 1px solid $border-color; diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 0c96c62c17..933ac7ae22 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -114,8 +114,8 @@ @media (max-width: map-get($grid-breakpoints, "lg")) { .page-content-wrapper .container { - padding-left: 1rem; - padding-right: 1rem; + padding-left: 1.5rem; + padding-right: 1.5rem; } } diff --git a/frappe/public/scss/website/markdown.scss b/frappe/public/scss/website/markdown.scss index c2592b61e9..2b17c209cd 100644 --- a/frappe/public/scss/website/markdown.scss +++ b/frappe/public/scss/website/markdown.scss @@ -5,7 +5,6 @@ } .from-markdown { - color: $gray-700; line-height: 1.7; > :first-child { @@ -30,7 +29,15 @@ } p, li { - font-size: $font-size-lg; + line-height: 1.7; + + @include media-breakpoint-up(sm) { + font-size: 1.05rem; + } + } + + p.lead { + @extend .lead; } li { diff --git a/frappe/public/scss/website/page_builder.scss b/frappe/public/scss/website/page_builder.scss index 21058dcf53..2ca51067b7 100644 --- a/frappe/public/scss/website/page_builder.scss +++ b/frappe/public/scss/website/page_builder.scss @@ -16,16 +16,18 @@ } } -.hero-title, .hero-subtitle { - max-width: 42rem; - margin-top: 0rem; - margin-bottom: 0.5rem; -} - .lead { + color: var(--text-muted); font-weight: normal; font-size: 1.25rem; + + margin-top: -0.5rem; margin-bottom: 1.5rem; + + @include media-breakpoint-up(sm) { + margin-top: -1rem; + margin-bottom: 2.5rem; + } } .hero-subtitle { @@ -38,6 +40,12 @@ } } +.hero-title, .hero-subtitle { + max-width: 42rem; + margin-top: 0rem; + margin-bottom: 0.5rem; +} + .hero.align-center { h1, .hero-title, .hero-subtitle, .hero-buttons { text-align: center; @@ -51,6 +59,7 @@ .section-description { max-width: 56rem; + color: var(--text-muted); margin-top: 0.5rem; font-size: $font-size-lg; @@ -549,7 +558,7 @@ font-weight: 600; @include media-breakpoint-up(md) { - font-size: $font-size-2xl; + font-size: $font-size-xl; } } diff --git a/frappe/templates/includes/comments/comment.html b/frappe/templates/includes/comments/comment.html index e0fc1c3c54..4713ee498d 100644 --- a/frappe/templates/includes/comments/comment.html +++ b/frappe/templates/includes/comments/comment.html @@ -1,14 +1,18 @@ {% from "frappe/templates/includes/avatar_macro.html" import avatar %} -
{{ blog_title or _('Blog') }}
-{{ blog_introduction or '' }}
+{{ blog_title or _('Blog') }}
+{{ blog_introduction or '' }}
{{ subtitle }}
{%- endif -%} -{{ video.title }}
+{{ video.title }}
{%- endif -%} {%- if video.content -%}{{ video.content }}
diff --git a/package.json b/package.json index d2c1de7be5..fd27bc223b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "js-sha256": "^0.9.0", "jsbarcode": "^3.9.0", "localforage": "^1.9.0", - "moment": "^2.20.1", + "moment": "^2.29.2", "moment-timezone": "^0.5.28", "node-sass": "^7.0.0", "plyr": "^3.6.2", diff --git a/yarn.lock b/yarn.lock index 339e4c5669..12021982ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2998,10 +2998,10 @@ moment-timezone@^0.5.28: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.20.1: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +"moment@>= 2.9.0", moment@^2.29.2: + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== ms@2.0.0: version "2.0.0"