diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index f2f43f10f8..f342c0709e 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -105,3 +105,5 @@ jobs: - name: UI Tests run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID + env: + CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb diff --git a/CODEOWNERS b/CODEOWNERS index 92723ab035..2dff157294 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,13 +4,10 @@ # the repo. Unless a later match takes precedence, * @frappe/frappe-review-team -website/ @prssanna -web_form/ @prssanna templates/ @surajshetty3416 www/ @surajshetty3416 integrations/ @leela patches/ @surajshetty3416 -dashboard/ @prssanna email/ @leela event_streaming/ @ruchamahabal data_import* @netchampfaris diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js new file mode 100644 index 0000000000..d12be63f3b --- /dev/null +++ b/cypress/integration/form_tour.js @@ -0,0 +1,88 @@ +context('Form Tour', () => { + before(() => { + cy.login(); + cy.visit('/app/form-tour'); + return cy.window().its('frappe').then(frappe => { + return frappe.call("frappe.tests.ui_test_helpers.create_form_tour"); + }); + }); + + const open_test_form_tour = () => { + cy.visit('/app/form-tour/Test Form Tour'); + cy.get('button[data-label="Show%20Tour"]').should('be.visible').and('contain', 'Show Tour').as('show_tour'); + cy.get('@show_tour').click(); + cy.wait(500); + cy.url().should('include', '/app/contact'); + }; + + it('jump to a form tour', open_test_form_tour); + + it('navigates a form tour', () => { + open_test_form_tour(); + + cy.get('#driver-popover-item').should('be.visible'); + cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name'); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + cy.get('.driver-next-btn').as('next_btn'); + + // next btn shouldn't move to next step, if first name is not entered + cy.get('@next_btn').click(); + cy.wait(500); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('first_name', 'Test Name', 'Data'); + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name'); + cy.get('@last_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('last_name', 'Test Last Name', 'Data'); + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos'); + cy.get('@phone_nos').should('have.class', 'driver-highlighted-element'); + + // move to next step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert add row btn is highlighted + cy.get('@phone_nos').find('.grid-add-row').as('add_row'); + cy.get('@add_row').should('have.class', 'driver-highlighted-element'); + + // add a row & move to next step + cy.wait(500); + cy.get('@add_row').click(); + cy.wait(500); + + // assert table field is highlighted + cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone'); + cy.get('@phone').should('have.class', 'driver-highlighted-element'); + // enter value in a table field + cy.fill_table_field('phone_nos', '1', 'phone', '1234567890'); + + // move to collapse row step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // collapse row + cy.get('.grid-row-open .grid-collapse-row').click(); + cy.wait(500); + + // assert save btn is highlighted + cy.get('.primary-action').should('have.class', 'driver-highlighted-element'); + cy.get('@next_btn').should('contain', 'Save'); + + }); +}); + \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index 01c7879a06..1c978945c7 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1683,7 +1683,7 @@ def get_desk_link(doctype, name): ) def bold(text): - return '{0}'.format(text) + return '{0}'.format(text) def safe_eval(code, eval_globals=None, eval_locals=None): '''A safer `eval`''' diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index c16de497ec..ca58e78870 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -661,7 +661,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None): frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile") # run for headless mode - run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' + run_or_open = 'run --browser firefox --record' if headless else 'open' command = '{site_env} {password_env} {cypress} {run_or_open}' formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open) @@ -770,19 +770,23 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False): @click.command('version') def get_version(): "Show the versions of all the installed apps" + from git import Repo from frappe.utils.change_log import get_app_branch frappe.init('') - for m in sorted(frappe.get_all_apps()): - branch_name = get_app_branch(m) - module = frappe.get_module(m) - app_hooks = frappe.get_module(m + ".hooks") + for app in sorted(frappe.get_all_apps()): + branch_name = get_app_branch(app) + module = frappe.get_module(app) + app_hooks = frappe.get_module(app + ".hooks") + repo = Repo(frappe.get_app_path(app, "..")) + branch = repo.head.ref.name + commit = repo.head.ref.commit.hexsha[:7] if hasattr(app_hooks, '{0}_version'.format(branch_name)): - print("{0} {1}".format(m, getattr(app_hooks, '{0}_version'.format(branch_name)))) + click.echo("{0} {1} {2} ({3})".format(app, getattr(app_hooks, '{0}_version'.format(branch_name)), branch, commit)) elif hasattr(module, "__version__"): - print("{0} {1}".format(m, module.__version__)) + click.echo("{0} {1} {2} ({3})".format(app, module.__version__, branch, commit)) @click.command('rebuild-global-search') diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index d35c118550..7ffbe6781d 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -85,8 +85,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = if attachments: add_attachments(comm.name, attachments) - frappe.db.commit() - if cint(send_email): if not comm.get_outgoing_email_account(): frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError) diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index 54a7788a2d..7a4d185d8f 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -62,9 +62,9 @@ class TestImporter(unittest.TestCase): data_import.reload() import_log = frappe.parse_json(data_import.import_log) self.assertEqual(import_log[0]['row_indexes'], [2,3]) - expected_error = "Error: Child 1 of DocType for Import Row #1: Value missing for: Child Title" + expected_error = "Error: Child 1 of DocType for Import Row #1: Value missing for: Child Title" self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error) - expected_error = "Error: Child 1 of DocType for Import Row #2: Value missing for: Child Title" + expected_error = "Error: Child 1 of DocType for Import Row #2: Value missing for: Child Title" self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error) self.assertEqual(import_log[1]['row_indexes'], [4]) diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index f00f729415..273b2654bf 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -25,7 +25,6 @@ def get_event_conditions(doctype, filters=None): @frappe.whitelist() def get_events(doctype, start, end, field_map, filters=None, fields=None): - field_map = frappe._dict(json.loads(field_map)) fields = frappe.parse_json(fields) @@ -36,8 +35,7 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None): "color": d.fieldname }) - if filters: - filters = json.loads(filters or '') + filters = json.loads(filters) if filters else [] if not fields: fields = [field_map.start, field_map.end, field_map.title, 'name'] @@ -52,5 +50,5 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None): [doctype, start_date, '<=', end], [doctype, end_date, '>=', start], ] - + fields = list({field for field in fields if field}) return frappe.get_list(doctype, fields=fields, filters=filters) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index a0d325e104..8de6c1301d 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -368,6 +368,7 @@ def get_desktop_page(page): 'allow_customization': not wspace.doc.disable_user_customization } except DoesNotExistError: + frappe.log_error(frappe.get_traceback()) return {} @frappe.whitelist() diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 94c6806b50..efb853cfa5 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,6 +3,45 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { + if (!frm.doc.is_standard || frappe.boot.developer_mode) { + frm.trigger('setup_queries'); + } + }, + + refresh(frm) { + if (frm.doc.is_standard && !frappe.boot.developer_mode) { + frm.trigger("disable_form"); + } + + frm.add_custom_button(__('Show Tour'), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + + if (issingle) { + frappe.set_route('Form', frm.doc.reference_doctype); + } else { + const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1'; + frappe.set_route('Form', frm.doc.reference_doctype, new_name); + } + frappe.utils.sleep(500).then(() => { + const tour_name = frm.doc.name; + cur_frm.tour + .init({ tour_name }) + .then(() => cur_frm.tour.start()); + }); + }); + }, + + disable_form: function(frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, + + setup_queries(frm) { frm.set_query("reference_doctype", function() { return { filters: { @@ -20,5 +59,65 @@ frappe.ui.form.on('Form Tour', { } }; }); + + frm.set_query("parent_field", "steps", function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: frm.doc.reference_doctype, + fieldtype: "Table", + hidden: 0, + } + }; + }); + + frm.trigger('reference_doctype'); + }, + + reference_doctype(frm) { + if (!frm.doc.reference_doctype) return; + + frappe.db.get_list('DocField', { + filters: { + parent: frm.doc.reference_doctype, + parenttype: 'DocType', + fieldtype: 'Table' + }, + fields: ['options'] + }).then(res => { + if (Array.isArray(res)) { + frm.child_doctypes = res.map(r => r.options); + } + }); + } }); + +frappe.ui.form.on('Form Tour Step', { + parent_field(frm, cdt, cdn) { + const child_row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'field', ''); + const field_control = get_child_field("steps", cdn, "field"); + field_control.get_query = function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: child_row.child_doctype, + hidden: 0 + } + }; + }; + } +}); + +function get_child_field(child_table, child_name, fieldname) { + // gets the field from grid row form + const grid = cur_frm.fields_dict[child_table].grid; + const grid_row = grid.grid_rows_by_docname[child_name]; + return grid_row.grid_form.fields_dict[fieldname]; +} + +async function check_if_single(doctype) { + const { message } = await frappe.db.get_value('DocType', doctype, 'issingle'); + return message.issingle || 0; +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 8e09a5d63a..e4ea528fcc 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -8,7 +8,9 @@ "field_order": [ "title", "reference_doctype", - "completed", + "module", + "is_standard", + "save_on_complete", "section_break_3", "steps" ], @@ -19,23 +21,16 @@ "in_list_view": 1, "label": "Reference Document", "options": "DocType", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { + "depends_on": "reference_doctype", "fieldname": "steps", "fieldtype": "Table", "label": "Steps", "options": "Form Tour Step", "reqd": 1 }, - { - "default": "0", - "depends_on": "eval: doc.__islocal != 1", - "fieldname": "completed", - "fieldtype": "Check", - "label": "Mark as Completed" - }, { "fieldname": "section_break_3", "fieldtype": "Section Break" @@ -46,11 +41,32 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "save_on_complete", + "fieldtype": "Check", + "label": "Save on Completion" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "fetch_from": "reference_doctype.module", + "fieldname": "module", + "fieldtype": "Link", + "hidden": 1, + "label": "Module", + "options": "Module Def", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-05-26 19:36:59.093753", + "modified": "2021-06-06 20:32:54.068774", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index dd762395c4..dbc667ce28 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -3,9 +3,33 @@ import frappe from frappe.model.document import Document +from frappe.modules.export_file import export_to_files class FormTour(Document): - pass + def before_insert(self): + if not self.is_standard: + return + + # while syncing, set proper docfield reference + for d in self.steps: + if not frappe.db.exists('DocField', d.field): + d.field = frappe.db.get_value('DocField', { + 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype + }, "name") + + if d.is_table_field and not frappe.db.exists('DocField', d.parent_field): + d.parent_field = frappe.db.get_value('DocField', { + 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table' + }, "name") + + def on_update(self): + if frappe.conf.developer_mode and self.is_standard: + export_to_files([['Form Tour', self.name]], self.module) + + def before_export(self, doc): + for d in doc.steps: + d.field = "" + d.parent_field = "" @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -16,17 +40,23 @@ def get_docfield_list(doctype, txt, searchfield, start, page_len, filters): ['fieldtype', 'like', '%' + txt + '%'] ] - parent_doctype = filters.pop('doctype') - excluded_fieldtypes = ['Column Break'] - excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + parent_doctype = filters.get('doctype') + fieldtype = filters.get('fieldtype') + if not fieldtype: + excluded_fieldtypes = ['Column Break'] + excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + fieldtype_filter = ['not in', excluded_fieldtypes] + else: + fieldtype_filter = fieldtype docfields = frappe.get_all( doctype, fields=["name as value", "label", "fieldtype"], - filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]}, + filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter}, or_filters=or_filters, limit_start=start, limit_page_length=page_len, + order_by="idx", as_list=1, ) return docfields diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index a772a2498a..3b6c91a208 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -4,14 +4,22 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "is_table_field", + "section_break_2", + "parent_field", "field", "title", "description", "column_break_2", "position", - "fieldname", "label", - "condition" + "has_next_condition", + "next_step_condition", + "section_break_13", + "fieldname", + "parent_fieldname", + "fieldtype", + "child_doctype" ], "fields": [ { @@ -30,6 +38,7 @@ "reqd": 1 }, { + "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))", "fieldname": "field", "fieldtype": "Link", "label": "Field", @@ -64,16 +73,73 @@ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center" }, { + "depends_on": "has_next_condition", "fieldname": "next_step_condition", "fieldtype": "Code", "label": "Next Step Condition", + "oldfieldname": "condition", "options": "JS" + }, + { + "default": "0", + "fieldname": "has_next_condition", + "fieldtype": "Check", + "label": "Has Next Condition" + }, + { + "default": "0", + "fetch_from": "field.fieldtype", + "fieldname": "fieldtype", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldtype", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_table_field", + "fieldtype": "Check", + "label": "Is Table Field" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "depends_on": "is_table_field", + "fieldname": "parent_field", + "fieldtype": "Link", + "label": "Parent Field", + "mandatory_depends_on": "is_table_field", + "options": "DocField" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Hidden Fields" + }, + { + "fetch_from": "parent_field.options", + "fieldname": "child_doctype", + "fieldtype": "Data", + "hidden": 1, + "label": "Child Doctype", + "read_only": 1 + }, + { + "fetch_from": "parent_field.fieldname", + "fieldname": "parent_fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Parent Fieldname", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-26 19:44:48.737453", + "modified": "2021-06-06 20:52:21.076972", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", @@ -82,4 +148,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index b0b03ca2f0..ed542a0fd2 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -45,19 +45,19 @@ class Workspace(Document): def get_link_groups(self): cards = [] - current_card = { + current_card = frappe._dict({ "label": "Link", "type": "Card Break", "icon": None, "hidden": False, - } + }) card_links = [] for link in self.links: link = link.as_dict() if link.type == "Card Break": - if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')): + if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')): current_card['links'] = card_links cards.append(current_card) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index af696e116d..9447e60529 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -83,11 +83,15 @@ class BaseDocument(object): @property def meta(self): - if not hasattr(self, "_meta"): + if not getattr(self, "_meta", None): self._meta = frappe.get_meta(self.doctype) return self._meta + def __getstate__(self): + self._meta = None + return self.__dict__ + def update(self, d): """ Update multiple fields of a doctype using a dictionary of key-value pairs. diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 28f9deb25d..836f70dd55 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -82,7 +82,7 @@ def get_doc_files(files, start_path): document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'web_template', 'notification', 'print_style', 'data_migration_mapping', 'data_migration_plan', 'workspace', - 'onboarding_step', 'module_onboarding'] + 'onboarding_step', 'module_onboarding', 'form_tour'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 233bbe0ce7..908479fd02 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -113,22 +113,20 @@ frappe.ui.form.PrintView = class { }, ).$input; - this.letterhead_selector = this.add_sidebar_item( + this.letterhead_selector_df = this.add_sidebar_item( { - fieldtype: 'Select', + fieldtype: 'Autocomplete', fieldname: 'letterhead', label: __('Select Letterhead'), - options: [ - this.get_default_option_for_select(__('Select Letterhead')), - __('No Letterhead') - ], + placeholder: __('Select Letterhead'), + options: [__('No Letterhead')], change: () => this.preview(), default: this.print_settings.with_letterhead ? __('No Letterhead') : __('Select Letterhead') }, - ).$input; - + ); + this.letterhead_selector = this.letterhead_selector_df.$input; this.sidebar_dynamic_section = $( `
` ).appendTo(this.sidebar); @@ -336,23 +334,19 @@ frappe.ui.form.PrintView = class { } set_letterhead_options() { - let letterhead_options = [ - this.get_default_option_for_select(__('Select Letterhead')), - __('No Letterhead') - ]; + let letterhead_options = [__('No Letterhead')]; let default_letterhead; let doc_letterhead = this.frm.doc.letter_head; return frappe.db - .get_list('Letter Head', { fields: ['name', 'is_default'] }) + .get_list('Letter Head', { fields: ['name', 'is_default'], limit: 0 }) .then((letterheads) => { - this.letterhead_selector.empty(); letterheads.map((letterhead) => { if (letterhead.is_default) default_letterhead = letterhead.name; return letterhead_options.push(letterhead.name); }); - this.letterhead_selector.add_options(letterhead_options); + this.letterhead_selector_df.set_data(letterhead_options); let selected_letterhead = doc_letterhead || default_letterhead; if (selected_letterhead) this.letterhead_selector.val(selected_letterhead); diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 89054e3791..2467302c76 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -10,7 +10,7 @@ frappe.db = { if (!args.fields) { args.fields = ['name']; } - if (!args.limit) { + if (!('limit' in args)) { args.limit = 20; } return new Promise ((resolve) => { diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 7de0ddd2b5..04b44a4ec8 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -607,9 +607,7 @@ frappe.Application = class Application { let doc = JSON.parse(pasted_data); if (doc.doctype) { e.preventDefault(); - let sleep = (time) => { - return new Promise((resolve) => setTimeout(resolve, time)); - }; + const sleep = frappe.utils.sleep; frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...'); // to avoid abrupt UX diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index eb7a6edc5d..6833f68073 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -179,7 +179,7 @@ frappe.ui.form.Dashboard = class FormDashboard { return; } this.render_links(); - this.set_open_count(); + // this.set_open_count(); show = true; } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index a24c6ab0d6..8064f90a98 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -12,6 +12,7 @@ import './script_manager'; import './script_helpers'; import './sidebar/form_sidebar'; import './footer/footer'; +import './form_tour'; frappe.ui.form.Controller = class FormController { constructor(opts) { @@ -152,6 +153,10 @@ frappe.ui.form.Form = class FrappeForm { parent: $('
').insertAfter(this.layout.wrapper.find('.form-message')) }); + this.tour = new frappe.ui.form.FormTour({ + frm: this + }); + // workflow state this.states = new frappe.ui.form.States({ frm: this @@ -987,7 +992,7 @@ frappe.ui.form.Form = class FrappeForm { } frappe.re_route[frappe.router.get_sub_path()] = `${encodeURIComponent(frappe.router.slug(this.doctype))}/${encodeURIComponent(name)}`; - frappe.set_route('Form', this.doctype, name); + !frappe._from_link && frappe.set_route('Form', this.doctype, name); } // ACTIONS @@ -1606,53 +1611,6 @@ frappe.ui.form.Form = class FrappeForm { }, 1000); } - show_tour(on_finish) { - const tour_info = frappe.tour[this.doctype]; - - if (!Array.isArray(tour_info)) { - return; - } - - const driver = new frappe.Driver({ - className: 'frappe-driver', - allowClose: false, - padding: 10, - overlayClickNext: true, - keyboardControl: true, - nextBtnText: 'Next', - prevBtnText: 'Previous', - opacity: 0.25 - }); - - this.layout.sections.forEach(section => section.collapse(false)); - - let steps = tour_info.map(step => { - let field = this.get_docfield(step.fieldname); - return { - element: `.frappe-control[data-fieldname='${step.fieldname}']`, - popover: { - title: step.title || field.label, - description: step.description, - position: step.position || 'bottom' - }, - onNext: () => { - const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true); - if (!next_condition_satisfied) { - driver.preventMove(); - } - - if (!driver.hasNextStep()) { - on_finish && on_finish(); - } - } - }; - }); - - driver.defineSteps(steps); - frappe.router.on('change', () => driver.reset()); - driver.start(); - } - setup_docinfo_change_listener() { let doctype = this.doctype; let docname = this.docname; diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js new file mode 100644 index 0000000000..7f7ec9ce4f --- /dev/null +++ b/frappe/public/js/frappe/form/form_tour.js @@ -0,0 +1,252 @@ +frappe.ui.form.FormTour = class FormTour { + constructor({ frm }) { + this.frm = frm; + this.driver_steps = []; + + this.init_driver(); + } + + init_driver() { + this.driver = new frappe.Driver({ + className: 'frappe-driver', + allowClose: false, + padding: 10, + overlayClickNext: true, + keyboardControl: true, + nextBtnText: 'Next', + prevBtnText: 'Previous', + opacity: 0.25, + onHighlighted: (step) => { + // if last step is to save, then attach a listener to save button + if (step.options.is_save_step) { + $(step.options.element).one('click', () => this.driver.reset()); + } + + // focus on input + const $input = $(step.node).find('input').get(0); + if ($input) + frappe.utils.sleep(200).then(() => $input.focus()); + } + }); + + frappe.router.on('change', () => this.driver.reset()); + this.frm.layout.sections.forEach(section => section.collapse(false)); + } + + async init({ tour_name, on_finish }) { + if (tour_name) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + } else { + this.tour = { steps: frappe.tour[this.frm.doctype] }; + } + + if (on_finish) this.on_finish = on_finish; + + this.build_steps(); + this.update_driver_steps(); + } + + build_steps() { + this.driver_steps = []; + this.tour.steps.forEach((step) => { + const on_next = () => { + if (!this.is_next_condition_satisfied(step)) { + this.driver.preventMove(); + } + + if (!this.driver.hasNextStep()) { + this.on_finish && this.on_finish(); + } + }; + + const driver_step = this.get_step(step, on_next); + this.driver_steps.push(driver_step); + + if (step.fieldtype == 'Table') this.handle_table_step(step); + if (step.is_table_field) this.handle_child_table_step(step); + }); + + if (this.tour.save_on_complete) { + this.add_step_to_save(); + } + } + + is_next_condition_satisfied(step) { + const form = step.is_table_field ? this.frm.cur_grid.grid_form : this.frm; + return form.layout.evaluate_depends_on_value(step.next_step_condition || true); + } + + get_step(step_info, on_next) { + const { name, fieldname, title, description, position, is_table_field } = step_info; + const field = this.frm.get_field(fieldname); + let element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + + if (is_table_field) { + element = `.grid-row-open .frappe-control[data-fieldname='${fieldname}']`; + } + + return { + element, + name, + popover: { title, description, position: frappe.router.slug(position) }, + onNext: on_next + }; + } + + update_driver_steps(steps = []) { + if (steps.length == 0) { + steps = this.driver_steps; + } + this.driver.defineSteps(steps); + } + + start(idx = 0) { + if (this.driver_steps.length == 0) { + return; + } + this.driver.start(idx); + } + + get_next_step() { + // returns the next step only if driver is active + if (this.driver.isActivated & this.driver.hasNextStep()) { + const current_step = this.driver.currentStep; + return this.driver.steps[current_step + 1]; + } + return; + } + + handle_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + // if next step field is inside currently highlighted table field + // then check if there is a row -> if not, then prompt to add row + // then edit the first row and hightlight next step + + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; + + if (!is_next_field_in_curr_table) return; + + const rows = this.frm.doc[curr_step.fieldname]; + const table_has_rows = rows && rows.length > 0; + if (table_has_rows) { + // table already has rows + // then just edit the first one on next step + const curr_driver_step = this.driver_steps.find(s => s.name == curr_step.name); + curr_driver_step.onNext = () => { + if (this.is_next_condition_satisfied(curr_step)) { + this.expand_row_and_proceed(curr_step, curr_step.idx); + } else { + this.driver.preventMove(); + } + }; + this.update_driver_steps(); + + } else { + this.add_new_row_step(curr_step); + } + } + } + + add_new_row_step(step) { + const $add_row = `.frappe-control[data-fieldname='${step.fieldname}'] .grid-add-row`; + const add_row_step = { + element: $add_row, + popover: { title: __("Add a Row"), description: "" }, + onNext: () => { + if (!cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(add_row_step); + + // setup a listener on add row button + // so, once the row is added, move to next step automatically + $($add_row).one('click', () => { + this.expand_row_and_proceed(step, step.idx + 1); // +1 since add row step is added + }); + } + + expand_row_and_proceed(step, start_from) { + this.open_first_row_of(step.fieldname); + this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM + frappe.utils.sleep(300).then(() => this.driver.start(start_from)); + } + + open_first_row_of(fieldname) { + this.frm.fields_dict[fieldname].grid.grid_rows[0].toggle_view(); + + // setup a listener on close row button + // so, once the row is closed, move to next step automatically + const $close_row = '.grid-row-open .grid-collapse-row'; + $($close_row).one('click', () => { + const next_step = this.get_next_step(); + const next_element = next_step.options.is_save_step ? null : next_step.node; + + frappe.utils.scroll_to(next_element, true, 150, null, () => { + this.driver.moveNext(); + frappe.flags.disable_auto_scroll = false; + }); + frappe.flags.disable_auto_scroll = true; + }); + } + + handle_child_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const field = this.frm.get_field(next_step.fieldname); + + if (!field) return; + + // next step highlights parent field + // so, add a step to prompt user to collapse grid form + this.add_collapse_row_step(); + + } else if (this.tour.save_on_complete) { + // if last step & save on complete is checked + // add a step to prompt user to collapse grid form + // to be able to save as a last step + this.add_collapse_row_step(); + } + } + + add_collapse_row_step() { + const $close_row = '.grid-row-open .grid-collapse-row'; + const close_row_step = { + element: $close_row, + popover: { title: __("Collapse"), description: "", position: "left" }, + onNext: () => { + if (cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(close_row_step); + } + + add_step_to_save() { + const page_id = `#page-${this.frm.doctype}`; + const $save_btn = `${page_id} .standard-actions .primary-action`; + const save_step = { + element: $save_btn, + is_save_step: true, + allowClose: false, + overlayClickNext: false, + popover: { + title: __("Save"), + description: "", + position: "left", + doneBtnText: __("Save") + } + }; + this.driver_steps.push(save_step); + frappe.ui.form.on(this.frm.doctype, 'after_save', () => this.on_finish && this.on_finish()); + } +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 282655b589..045e5dc1b3 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 Layout { } else if (expression.substr(0, 5)=='eval:') { try { - out = frappe.utils.eval(expression.substr(5), { doc }); + out = frappe.utils.eval(expression.substr(5), { doc, parent }); if (parent && parent.istable && expression.includes('is_submittable')) { out = true; } diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index 4f116df63e..538534e5cf 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -11,7 +11,7 @@ frappe.ui.form.Attachments = class Attachments { this.parent.find(".add-attachment-btn").click(function() { me.new_attachment(); }); - this.add_attachment_wrapper = this.parent.find(".add_attachment").parent(); + this.add_attachment_wrapper = this.parent.find(".add-attachment-btn"); this.attachments_label = this.parent.find(".attachments-label"); } max_reached(raise_exception=false) { @@ -39,7 +39,7 @@ frappe.ui.form.Attachments = class Attachments { this.parent.find(".attachment-row").remove(); var max_reached = this.max_reached(); - this.add_attachment_wrapper.toggleClass("hide", !max_reached); + this.add_attachment_wrapper.toggle(!max_reached); // add attachment objects var attachments = this.get_attachments(); diff --git a/frappe/public/js/frappe/form/templates/form_links.html b/frappe/public/js/frappe/form/templates/form_links.html index e16516e652..57edb69a15 100644 --- a/frappe/public/js/frappe/form/templates/form_links.html +++ b/frappe/public/js/frappe/form/templates/form_links.html @@ -5,7 +5,7 @@ {% } %}
{% for (let j=0; j < transactions[i].items.length; j++) { %} {% let doctype = transactions[i].items[j]; %} diff --git a/frappe/public/js/frappe/form/templates/report_links.html b/frappe/public/js/frappe/form/templates/report_links.html index 3b69586d32..b820ec3a52 100644 --- a/frappe/public/js/frappe/form/templates/report_links.html +++ b/frappe/public/js/frappe/form/templates/report_links.html @@ -5,7 +5,7 @@ {% } %}
{% for (let j=0; j < reports[i].items.length; j++) { %} {% let report = reports[i].items[j]; %} diff --git a/frappe/public/js/frappe/ui/capture.js b/frappe/public/js/frappe/ui/capture.js index 676c997b31..d408fadb33 100644 --- a/frappe/public/js/frappe/ui/capture.js +++ b/frappe/public/js/frappe/ui/capture.js @@ -116,10 +116,7 @@ frappe.ui.Capture = class { }) .catch(err => { if (this.options.error) { - const alert = ` ${ - frappe.ui.Capture.ERR_MESSAGE - }`; - frappe.show_alert(alert, 3); + frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3); } throw err; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 9e20e419be..a366ca15a3 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -268,7 +268,9 @@ Object.assign(frappe.utils, {

'); return content.html(); }, - scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) { + scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled, callback) { + if (frappe.flags.disable_auto_scroll) return; + element_to_be_scrolled = element_to_be_scrolled || $("html, body"); let scroll_top = 0; if (element) { @@ -289,7 +291,7 @@ Object.assign(frappe.utils, { } if (animate) { - element_to_be_scrolled.animate({ scrollTop: scroll_top }); + element_to_be_scrolled.animate({ scrollTop: scroll_top }).promise().then(callback); } else { element_to_be_scrolled.scrollTop(scroll_top); } @@ -1332,5 +1334,9 @@ Object.assign(frappe.utils, { }); !prepend && button.appendTo(wrapper); prepend && wrapper.prepend(button); + }, + + sleep(time) { + return new Promise((resolve) => setTimeout(resolve, time)); } }); diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 87f692d125..0ab5e2e7dc 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -109,7 +109,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { frappe.views.Calendar = class Calendar { constructor(options) { $.extend(this, options); - this.field_map = { + this.field_map = this.field_map || { "id": "name", "start": "start", "end": "end", diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index e2aaec553d..0eeb5d9ffc 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -145,7 +145,7 @@ frappe.views.CommunicationComposer = class { ); }); - if (email_accounts.length > 1) { + if (email_accounts.length) { fields.unshift({ label: __("From"), fieldtype: "Select", @@ -728,7 +728,7 @@ frappe.views.CommunicationComposer = class { const SALUTATION_END_COMMENT = ""; if (this.real_name && !message.includes(SALUTATION_END_COMMENT)) { this.message = ` -

${__('Dear')} ${this.real_name},

+

${__('Dear {0},', [this.real_name], 'Salutation in new email')},

${SALUTATION_END_COMMENT}
${message} `; diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index 50f911c808..1d7ea37fe6 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -180,7 +180,7 @@ frappe.provide("frappe.views"); method_name = "update_order_for_single_card"; args = { board_name: this.board.name, - docname: unescape(card.name), + docname: card.name, from_colname: card.from_colname, to_colname: card.to_colname, old_index: card.old_index, @@ -222,7 +222,7 @@ frappe.provide("frappe.views"); var col_name = $(this).data().columnValue; order[col_name] = []; $(this).find('.kanban-card-wrapper').each(function() { - var card_name = unescape($(this).data().name); + var card_name = decodeURIComponent($(this).data().name); order[col_name].push(card_name); }); }); @@ -514,7 +514,7 @@ frappe.provide("frappe.views"); wrapper.find('.kanban-cards').height('auto'); // update order const args = { - name: $(e.item).attr('data-name'), + name: decodeURIComponent($(e.item).attr('data-name')), from_colname: $(e.from).parents('.kanban-column').attr('data-column-value'), to_colname: $(e.to).parents('.kanban-column').attr('data-column-value'), old_index: e.oldIndex, diff --git a/frappe/public/js/frappe/views/kanban/kanban_card.html b/frappe/public/js/frappe/views/kanban/kanban_card.html index b854b88d18..88cf366ccc 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_card.html +++ b/frappe/public/js/frappe/views/kanban/kanban_card.html @@ -1,4 +1,4 @@ -
+
{% if(image_url) { %} diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index cd9ae812b7..468cdfacb3 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -68,6 +68,7 @@ export default class LinksWidget extends Widget { const opts = { name: item.link_to, type: item.link_type, + doctype: item.doctype, is_query_report: item.is_query_report }; diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index e552a7dd55..b487c0134f 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -203,7 +203,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { let msg_dialog = frappe.msgprint({ message: __("Let's take you back to onboarding"), title: __("Great Job"), @@ -217,7 +217,10 @@ export default class OnboardingWidget extends Widget { label: () => __("Continue"), }, }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; frappe.set_route(route); @@ -290,12 +293,15 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { frappe.msgprint({ message: __("Awesome, now try making an entry yourself"), title: __("Great"), }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; let callback = () => { diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index b54607e8b6..48a8a48f5f 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -24,7 +24,7 @@ --blue-100: #D3E9FC; --blue-50 : #F0F8FE; - --cyan-900: #006464; + --cyan-900: #006464; --cyan-800: #007272; --cyan-700: #008b8b; --cyan-600: #02c5c5; @@ -179,6 +179,10 @@ --text-on-pink: var(--pink-500); --text-on-cyan: var(--cyan-600); + --disabled-control-bg: var(--gray-50); + --control-bg: var(--gray-100); + --control-bg-on-gray: var(--gray-200); + --awesomplete-hover-bg: var(--control-bg); // Other Colors @@ -208,5 +212,4 @@ --checkbox-right-margin: var(--margin-xs); --checkbox-size: 14px; --checkbox-focus-shadow: 0 0 0 2px var(--gray-300); - } diff --git a/frappe/public/scss/common/datepicker.scss b/frappe/public/scss/common/datepicker.scss index 93bdfcc03d..b21eb58caf 100644 --- a/frappe/public/scss/common/datepicker.scss +++ b/frappe/public/scss/common/datepicker.scss @@ -15,6 +15,10 @@ &--time-current-hours, &--time-current-minutes, &--time-current-seconds { font-family: inherit; + &:after { + color: var(--text-color); + background-color: var(--fg-hover-color); + } } &--day-name { @@ -47,10 +51,13 @@ background: fade(#0089FF, 10%); } + &.-focus- { + background-color: var(--fg-hover-color); + } + &.-selected-.-focus- { background: fade(#0089FF, 90%); } - } &--time, &--buttons { @@ -67,8 +74,20 @@ &--time-row:first-child { margin: 0; } + + &--pointer { + background: var(--fg-color); + border-top-right-radius: 2px; + border: 1px var(--border-color); + border-style: solid solid hidden hidden; + } + + &--button { + color: var(--brand-color); + &:hover { + color: var(--text-color); + background-color: var(--fg-hover-color); + } + } } -.datepicker--button { - color: var(--brand-color); -} diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 12706d6b7f..a7a690543b 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -35,13 +35,14 @@ .ql-container.ql-snow { border-bottom-left-radius: var(--border-radius); border-bottom-right-radius: var(--border-radius); - overflow: hidden; } .ql-snow { .ql-editor { min-height: 400px; max-height: 600px; + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); } .ql-stroke { stroke: var(--icon-stroke); diff --git a/frappe/public/scss/desk/driver.scss b/frappe/public/scss/desk/driver.scss index ddf594393f..4135d9667b 100644 --- a/frappe/public/scss/desk/driver.scss +++ b/frappe/public/scss/desk/driver.scss @@ -64,4 +64,16 @@ div#driver-popover-item { input.driver-highlighted-element { background-color: var(--fg-color); +} + +.driver-fix-stacking { + z-index: auto !important; + position: unset !important; + opacity: 1.0 !important; + transform: none !important; + filter: none !important; + perspective: none !important; + transform-style: flat !important; + transform-box: border-box !important; + will-change: unset !important; } \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 52ac70513d..4400578862 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -291,15 +291,19 @@ var verify_token = function (event) { } var request_otp = function (r) { - $('.login-content').empty().append($('
').attr({ 'id': 'twofactor_div' }).html( - '
\ -
\ - {{ _("Verification") }}\ -
\ -
\ - \ - \ -
')); + $('.login-content').empty(); + $('.login-content:visible').append( + `
+
+
+ {{ _("Verification") }} +
+
+ + +
+
` + ); // add event handler for submit button verify_token(); } diff --git a/frappe/templates/test/_test_base.html b/frappe/templates/test/_test_base.html index 17caf8df1b..1d5019df37 100644 --- a/frappe/templates/test/_test_base.html +++ b/frappe/templates/test/_test_base.html @@ -1,20 +1,9 @@ - {%- block style %} - {% if colocated_css -%} - - {%- endif %} - {%- endblock -%} - {% include "templates/includes/breadcrumbs.html" %}

This is for testing

{% block content %}{% endblock %} - {%- block script %} - {% if colocated_js -%} - - {%- endif %} - {%- endblock %} diff --git a/frappe/templates/test/_test_base_breadcrumbs.html b/frappe/templates/test/_test_base_breadcrumbs.html new file mode 100644 index 0000000000..17caf8df1b --- /dev/null +++ b/frappe/templates/test/_test_base_breadcrumbs.html @@ -0,0 +1,20 @@ + + + + {%- block style %} + {% if colocated_css -%} + + {%- endif %} + {%- endblock -%} + + + {% include "templates/includes/breadcrumbs.html" %} +

This is for testing

+ {% block content %}{% endblock %} + {%- block script %} + {% if colocated_js -%} + + {%- endif %} + {%- endblock %} + + diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 29939fea1c..32a5ebbd72 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -147,6 +147,7 @@ class TestResourceAPI(unittest.TestCase): response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}") self.assertEqual(response.status_code, 202) self.assertDictEqual(response.json(), {"message": "ok"}) + self.GENERATED_DOCUMENTS.remove(doc_to_delete) non_existent_doc = frappe.generate_hash(length=12) response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}") diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 6f265d9b94..f1c4f3b3f5 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -201,6 +201,85 @@ class TestWebsite(unittest.TestCase): self.assertIn('