From 0482530ffd3813b9cfb2b7ecb40d1c5b2ed181ca Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 3 Feb 2023 20:04:11 +0530 Subject: [PATCH 01/43] fix: do not allow restricted fieldnames for custom fields --- .../custom/doctype/custom_field/custom_field.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 758d9c1e64..8953153be6 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -18,6 +18,18 @@ class CustomField(Document): self.name = self.dt + "-" + self.fieldname def set_fieldname(self): + restricted = ( + "name", + "parent", + "creation", + "modified", + "modified_by", + "parentfield", + "parenttype", + "file_list", + "flags", + "docstatus", + ) if not self.fieldname: label = self.label if not label: @@ -34,6 +46,9 @@ class CustomField(Document): # fieldnames should be lowercase self.fieldname = self.fieldname.lower() + if self.fieldname in restricted: + self.fieldname = self.fieldname + "1" + def before_insert(self): self.set_fieldname() From 39761d3d7e8a5723c39edf87a241714d3ada3421 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 15 Feb 2023 16:55:28 +0530 Subject: [PATCH 02/43] feat(Calendar): Add a new option `convertToUserTz` to address timezone inconsistencies --- frappe/public/js/frappe/views/calendar/calendar.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index cba7886a93..4a22457dd7 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -120,6 +120,7 @@ frappe.views.Calendar = class Calendar { start: "start", end: "end", allDay: "all_day", + convertToUserTz: "convert_to_user_tz", }; this.color_map = { danger: "red", @@ -372,10 +373,13 @@ frappe.views.Calendar = class Calendar { }); if (!me.field_map.allDay) d.allDay = 1; + if (!me.field_map.convertToUserTz) d.convertToUserTz = 1; // convert to user tz - d.start = frappe.datetime.convert_to_user_tz(d.start); - d.end = frappe.datetime.convert_to_user_tz(d.end); + if (d.convertToUserTz) { + d.start = frappe.datetime.convert_to_user_tz(d.start); + d.end = frappe.datetime.convert_to_user_tz(d.end); + } // show event on single day if start or end date is invalid if (!frappe.datetime.validate(d.start) && d.end) { From 80a49329831233a4472e0bd8d868b875c3a10d68 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 16 Feb 2023 18:42:24 +0530 Subject: [PATCH 03/43] fix: ask before changing restricted fieldnames --- .../doctype/customize_form/customize_form.js | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index fed8505147..73981eaf28 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -310,22 +310,53 @@ frappe.ui.form.on("DocType State", { }, }); -frappe.customize_form.set_primary_action = function (frm) { - frm.page.set_primary_action(__("Update"), function () { - if (frm.doc.doc_type) { - return frm.call({ - doc: frm.doc, - freeze: true, - btn: frm.page.btn_primary, - method: "save_customization", - callback: function (r) { - if (!r.exc) { - frappe.customize_form.clear_locals_and_refresh(frm); - frm.script_manager.trigger("doc_type"); - } - }, - }); +frappe.customize_form.validate_fieldnames = async function (frm) { + for (let i = 0; i < frm.doc.fields.length; i++) { + let field = frm.doc.fields[i]; + + let fieldname = field.label && frappe.model.scrub(field.label).toLowerCase(); + if ( + field.label && + !field.fieldname && + in_list(frappe.model.restricted_fields, fieldname) + ) { + let message = __( + "For field {0} in row {1}, fieldname {2} is restricted it will be renamed as {2}1. Do you want to continue?", + [field.label, field.idx, fieldname] + ); + await pause_to_confirm(message); } + } + + function pause_to_confirm(message) { + return new Promise((resolve) => { + frappe.confirm(message, () => resolve()); + }); + } +}; + +frappe.customize_form.save_customization = function (frm) { + if (frm.doc.doc_type) { + return frm.call({ + doc: frm.doc, + freeze: true, + freeze_message: __("Updating Customization..."), + btn: frm.page.btn_primary, + method: "save_customization", + callback: function (r) { + if (!r.exc) { + frappe.customize_form.clear_locals_and_refresh(frm); + frm.script_manager.trigger("doc_type"); + } + }, + }); + } +}; + +frappe.customize_form.set_primary_action = function (frm) { + frm.page.set_primary_action(__("Update"), async () => { + await this.validate_fieldnames(frm); + this.save_customization(frm); }); }; From 908545241bde7fe0561ac8b196f9c1e440e46a31 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 16 Feb 2023 19:31:07 +0530 Subject: [PATCH 04/43] fix: enable update button if fieldname change is rejected --- frappe/custom/doctype/customize_form/customize_form.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index a0be0f3d63..d1ee27faba 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -334,7 +334,13 @@ frappe.customize_form.validate_fieldnames = async function (frm) { function pause_to_confirm(message) { return new Promise((resolve) => { - frappe.confirm(message, () => resolve()); + frappe.confirm( + message, + () => resolve(), + () => { + frm.page.btn_primary.prop("disabled", false); + } + ); }); } }; From 649c211b9bce0fdae7399dc1c0a59dcf8cb4621e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 20 Feb 2023 10:13:49 +0530 Subject: [PATCH 05/43] fix(UX): show message when form is read only (#20077) [skip ci] --- frappe/public/js/frappe/form/form.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 001279f394..d1fe443f7a 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -406,7 +406,10 @@ frappe.ui.form.Form = class FrappeForm { // read only (workflow) this.read_only = frappe.workflow.is_read_only(this.doctype, this.docname); - if (this.read_only) this.set_read_only(true); + if (this.read_only) { + this.set_read_only(true); + frappe.show_alert(__("This form is not editable due to a Workflow.")); + } // check if doctype is already open if (!this.opendocs[this.docname]) { From 7afc46401b7c23620982745181ab659b636df1a3 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 20 Feb 2023 11:21:24 +0530 Subject: [PATCH 06/43] chore: changed freeze message --- frappe/custom/doctype/customize_form/customize_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index d1ee27faba..4ab693b415 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -350,7 +350,7 @@ frappe.customize_form.save_customization = function (frm) { return frm.call({ doc: frm.doc, freeze: true, - freeze_message: __("Updating Customization..."), + freeze_message: __("Saving Customization..."), btn: frm.page.btn_primary, method: "save_customization", callback: function (r) { From 89d63ea82b0fbd2d744aa890d1d49e0ad8804ffa Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 20 Feb 2023 12:18:37 +0530 Subject: [PATCH 07/43] fix: false positive attr check while applying permlevel (#20069) * fix: false positive attr check while applying permlevel * Revert "fix: false positive attr check while applying permlevel" This reverts commit 9114788590ce12be977df847c13b00e3bf72ac2a. * fix: ignore AttributeError while trying to pop low permlevel fields --------- Co-authored-by: Ankush Menat --- frappe/model/document.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 8a99676b60..7fcb9ac335 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -676,7 +676,11 @@ class Document(BaseDocument): for df in self.meta.fields: if df.permlevel and hasattr(self, df.fieldname) and df.permlevel not in has_access_to: - delattr(self, df.fieldname) + try: + delattr(self, df.fieldname) + except AttributeError: + # hasattr might return True for class attribute which can't be delattr-ed. + continue for table_field in self.meta.get_table_fields(): for df in frappe.get_meta(table_field.options).fields or []: From c94d3ccc16f4b674af2b0aabe446fd6aa1406aca Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 20 Feb 2023 12:51:24 +0530 Subject: [PATCH 08/43] fix: Hide perm level fields for Section, Column and Tab Breaks (#20084) --- frappe/core/doctype/docfield/docfield.json | 3 ++- .../doctype/customize_form_field/customize_form_field.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 0d8d7ea671..90b1c6cb77 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -304,6 +304,7 @@ }, { "default": "0", + "depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)", "fieldname": "permlevel", "fieldtype": "Int", "label": "Perm Level", @@ -555,7 +556,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-01-11 20:46:43.164926", + "modified": "2023-02-20 12:07:29.552523", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index ad0d600a0b..d8da44101b 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -212,6 +212,7 @@ }, { "default": "0", + "depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)", "fieldname": "permlevel", "fieldtype": "Int", "in_list_view": 1, @@ -467,7 +468,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-30 14:25:50.649449", + "modified": "2023-02-20 12:07:40.242470", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", From b55bbd0a8c3dde53565d6440a0d848f65aba056d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 20 Feb 2023 13:07:32 +0530 Subject: [PATCH 09/43] fix(UX): Sort case-insensitive where it makes sense (#20088) --- frappe/core/page/permission_manager/permission_manager.py | 4 ++-- frappe/desk/doctype/tag/tag.py | 2 +- frappe/desk/search.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 45c1e44fa1..5ed3014778 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -62,8 +62,8 @@ def get_roles_and_doctypes(): roles_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in roles] return { - "doctypes": sorted(doctypes_list, key=lambda d: d["label"]), - "roles": sorted(roles_list, key=lambda d: d["label"]), + "doctypes": sorted(doctypes_list, key=lambda d: d["label"].casefold()), + "roles": sorted(roles_list, key=lambda d: d["label"].casefold()), } diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 84239fae6d..c5fe6407b7 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -59,7 +59,7 @@ def get_tags(doctype, txt): tag = frappe.get_list("Tag", filters=[["name", "like", f"%{txt}%"]]) tags = [t.name for t in tag] - return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags)))) + return sorted(filter(lambda t: t and txt.casefold() in t.casefold(), list(set(tags)))) class DocTags: diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 2af9b575be..ee63f67423 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -282,7 +282,7 @@ def scrub_custom_query(query, key, txt): def relevance_sorter(key, query, as_dict): value = _(key.name if as_dict else key[0]) - return (cstr(value).lower().startswith(query.lower()) is not True, value) + return (cstr(value).casefold().startswith(query.casefold()) is not True, value) def validate_and_sanitize_search_inputs(fn): From 68df7d621f8489ddb4ec6c4a6ebe4cae7dd07389 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 20 Feb 2023 13:13:35 +0530 Subject: [PATCH 10/43] docs: document_naming_settings field label [skip ci] --- .../document_naming_settings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/document_naming_settings/document_naming_settings.json b/frappe/core/doctype/document_naming_settings/document_naming_settings.json index 4c86b2ec1d..9a12f3f77e 100644 --- a/frappe/core/doctype/document_naming_settings/document_naming_settings.json +++ b/frappe/core/doctype/document_naming_settings/document_naming_settings.json @@ -81,10 +81,10 @@ }, { "depends_on": "transaction_type", - "description": "Generate 3 preview of names generate by any valid series.", + "description": "Get a preview of generated names with a series.", "fieldname": "try_naming_series", "fieldtype": "Data", - "label": "Try a naming Series" + "label": "Try a Naming Series" }, { "fieldname": "transaction_type", @@ -111,7 +111,7 @@ "icon": "fa fa-sort-by-order", "issingle": 1, "links": [], - "modified": "2022-05-30 23:51:36.136535", + "modified": "2023-02-20 13:11:56.662100", "modified_by": "Administrator", "module": "Core", "name": "Document Naming Settings", @@ -130,4 +130,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From 7f73906528123c0ead88b21c765270f817b25749 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 20 Feb 2023 13:19:20 +0530 Subject: [PATCH 11/43] fix: switching from option store syntax to setup store syntax --- frappe/public/js/form_builder/store.js | 491 +++++++++++++------------ 1 file changed, 256 insertions(+), 235 deletions(-) diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js index 246956dc94..c337e471d6 100644 --- a/frappe/public/js/form_builder/store.js +++ b/frappe/public/js/form_builder/store.js @@ -1,270 +1,291 @@ import { defineStore } from "pinia"; import { create_layout, scrub_field_names } from "./utils"; -import { nextTick } from "vue"; +import { computed, nextTick, ref } from "vue"; -export const useStore = defineStore("form-builder-store", { - state: () => ({ - doctype: "", - doc: null, - docfields: [], - custom_docfields: [], - layout: {}, - active_tab: "", - selected_field: null, - dirty: false, - read_only: false, - is_customize_form: false, - preview: false, - drag: false, - }), - getters: { - get_animation: () => { - return "cubic-bezier(0.34, 1.56, 0.64, 1)"; - }, - selected: (state) => { - return (name) => state.selected_field?.name == name; - }, - get_docfields: (state) => { - return state.is_customize_form ? state.custom_docfields : state.docfields; - }, - get_df: (state) => { - return (fieldtype, fieldname = "", label = "") => { - let docfield = state.is_customize_form ? "Customize Form Field" : "DocField"; - let df = frappe.model.get_new_doc(docfield); - df.name = frappe.utils.get_random(8); - df.fieldtype = fieldtype; - df.fieldname = fieldname; - df.label = label; - state.is_customize_form && (df.is_custom_field = 1); - return df; - }; - }, - has_standard_field: (state) => { - return (field) => { - if (!state.is_customize_form) return; - if (!field.df.is_custom_field) return true; +export const useStore = defineStore("form-builder-store", () => { + let doctype = ref(""); + let doc = ref(null); + let docfields = ref([]); + let custom_docfields = ref([]); + let layout = ref({}); + let active_tab = ref(""); + let selected_field = ref(null); + let dirty = ref(false); + let read_only = ref(false); + let is_customize_form = ref(false); + let preview = ref(false); + let drag = ref(false); + let get_animation = ref("cubic-bezier(0.34, 1.56, 0.64, 1)"); - let children = { - "Tab Break": "sections", - "Section Break": "columns", - "Column Break": "fields", - }[field.df.fieldtype]; + // Getters + let get_docfields = computed(() => { + return is_customize_form.value ? custom_docfields.value : docfields.value; + }); - if (!children) return false; + let current_tab = computed(() => { + return layout.value.tabs.find((tab) => tab.df.name == active_tab.value); + }); - return field[children].some((child) => { - if (!child.df.is_custom_field) return true; - return state.has_standard_field(child); - }); - }; - }, - current_tab: (state) => { - return state.layout.tabs.find((tab) => tab.df.name == state.active_tab); - }, - }, - actions: { - async fetch() { - await frappe.model.clear_doc("DocType", this.doctype); - await frappe.model.with_doctype(this.doctype); + // Actions + function selected(name) { + return selected_field.value?.name == name; + } - if (this.is_customize_form) { - await frappe.model.with_doc("Customize Form"); - let doc = frappe.get_doc("Customize Form"); - doc.doc_type = this.doctype; - let r = await frappe.call({ method: "fetch_to_customize", doc }); - this.doc = r.docs[0]; + function get_df(fieldtype, fieldname = "", label = "") { + let docfield = is_customize_form.value ? "Customize Form Field" : "DocField"; + let df = frappe.model.get_new_doc(docfield); + df.name = frappe.utils.get_random(8); + df.fieldtype = fieldtype; + df.fieldname = fieldname; + df.label = label; + is_customize_form.value && (df.is_custom_field = 1); + return df; + } + + function has_standard_field(field) { + if (!is_customize_form.value) return; + if (!field.df.is_custom_field) return true; + + let children = { + "Tab Break": "sections", + "Section Break": "columns", + "Column Break": "fields", + }[field.df.fieldtype]; + + if (!children) return false; + + return field[children].some((child) => { + if (!child.df.is_custom_field) return true; + return has_standard_field(child); + }); + } + + async function fetch() { + await frappe.model.clear_doc("DocType", doctype.value); + await frappe.model.with_doctype(doctype.value); + + if (is_customize_form.value) { + await frappe.model.with_doc("Customize Form"); + let doc = frappe.get_doc("Customize Form"); + doc.doc_type = doctype.value; + let r = await frappe.call({ method: "fetch_to_customize", doc }); + doc.value = r.docs[0]; + } else { + doc.value = await frappe.db.get_doc("DocType", doctype.value); + } + + if (!get_docfields.value.length) { + let docfield = is_customize_form.value ? "Customize Form Field" : "DocField"; + await frappe.model.with_doctype(docfield); + let df = frappe.get_meta(docfield).fields; + if (is_customize_form.value) { + custom_docfields.value = df; } else { - this.doc = await frappe.db.get_doc("DocType", this.doctype); + docfields.value = df; + } + } + + layout.value = get_layout(); + active_tab.value = layout.value.tabs[0].df.name; + selected_field.value = null; + + nextTick(() => { + dirty.value = false; + read_only.value = + !is_customize_form.value && !frappe.boot.developer_mode && !doc.value.custom; + preview.value = false; + }); + } + + function reset_changes() { + fetch(); + } + + function validate_fields(fields, is_table) { + fields = scrub_field_names(fields); + + let not_allowed_in_list_view = ["Attach Image", ...frappe.model.no_value_type]; + if (is_table) { + not_allowed_in_list_view = not_allowed_in_list_view.filter((f) => f != "Button"); + } + + function get_field_data(df) { + let fieldname = `${df.label} (${df.fieldname})`; + if (!df.label) { + fieldname = `${df.fieldname}`; + } + let fieldtype = `${df.fieldtype}`; + return [fieldname, fieldtype]; + } + + fields.forEach((df) => { + // check if fieldname already exist + let duplicate = fields.filter((f) => f.fieldname == df.fieldname); + if (duplicate.length > 1) { + frappe.throw(__("Fieldname {0} appears multiple times", get_field_data(df))); } - if (!this.get_docfields.length) { - let docfield = this.is_customize_form ? "Customize Form Field" : "DocField"; - await frappe.model.with_doctype(docfield); - let df = frappe.get_meta(docfield).fields; - if (this.is_customize_form) { - this.custom_docfields = df; - } else { - this.docfields = df; - } + // Link & Table fields should always have options set + if (in_list(["Link", ...frappe.model.table_fields], df.fieldtype) && !df.options) { + frappe.throw( + __("Options is required for field {0} of type {1}", get_field_data(df)) + ); } - this.layout = this.get_layout(); - this.active_tab = this.layout.tabs[0].df.name; - this.selected_field = null; - - nextTick(() => { - this.dirty = false; - this.read_only = - !this.is_customize_form && !frappe.boot.developer_mode && !this.doc.custom; - this.preview = false; - }); - }, - reset_changes() { - this.fetch(); - }, - validate_fields(fields, is_table) { - fields = scrub_field_names(fields); - - let not_allowed_in_list_view = ["Attach Image", ...frappe.model.no_value_type]; - if (is_table) { - not_allowed_in_list_view = not_allowed_in_list_view.filter((f) => f != "Button"); + // Do not allow if field is hidden & required but doesn't have default value + if (df.hidden && df.reqd && !df.default) { + frappe.throw( + __( + "{0} cannot be hidden and mandatory without any default value", + get_field_data(df) + ) + ); } - function get_field_data(df) { - let fieldname = `${df.label} (${df.fieldname})`; - if (!df.label) { - fieldname = `${df.fieldname}`; - } - let fieldtype = `${df.fieldtype}`; - return [fieldname, fieldtype]; + // In List View is not allowed for some fieldtypes + if (df.in_list_view && in_list(not_allowed_in_list_view, df.fieldtype)) { + frappe.throw( + __( + "'In List View' is not allowed for field {0} of type {1}", + get_field_data(df) + ) + ); } - fields.forEach((df) => { - // check if fieldname already exist - let duplicate = fields.filter((f) => f.fieldname == df.fieldname); - if (duplicate.length > 1) { - frappe.throw(__("Fieldname {0} appears multiple times", get_field_data(df))); - } + // In Global Search is not allowed for no_value_type fields + if (df.in_global_search && in_list(frappe.model.no_value_type, df.fieldtype)) { + frappe.throw( + __( + "'In Global Search' is not allowed for field {0} of type {1}", + get_field_data(df) + ) + ); + } + }); + } - // Link & Table fields should always have options set - if (in_list(["Link", ...frappe.model.table_fields], df.fieldtype) && !df.options) { - frappe.throw( - __("Options is required for field {0} of type {1}", get_field_data(df)) - ); - } + async function save_changes() { + if (!dirty.value) { + frappe.show_alert({ message: __("No changes to save"), indicator: "orange" }); + return; + } - // Do not allow if field is hidden & required but doesn't have default value - if (df.hidden && df.reqd && !df.default) { - frappe.throw( - __( - "{0} cannot be hidden and mandatory without any default value", - get_field_data(df) - ) - ); - } + frappe.dom.freeze(__("Saving...")); - // In List View is not allowed for some fieldtypes - if (df.in_list_view && in_list(not_allowed_in_list_view, df.fieldtype)) { - frappe.throw( - __( - "'In List View' is not allowed for field {0} of type {1}", - get_field_data(df) - ) - ); - } + try { + if (is_customize_form.value) { + let doc = frappe.get_doc("Customize Form"); + doc.doc_type = doctype.value; + doc.fields = get_updated_fields(); + validate_fields(doc.fields, doc.istable); + await frappe.call({ method: "save_customization", doc }); + } else { + doc.value.fields = get_updated_fields(); + validate_fields(doc.value.fields, doc.value.istable); + await frappe.call("frappe.client.save", { doc: doc.value }); + frappe.toast("Fields Table Updated"); + } + fetch(); + } catch (e) { + console.error(e); + } finally { + frappe.dom.unfreeze(); + } + } - // In Global Search is not allowed for no_value_type fields - if (df.in_global_search && in_list(frappe.model.no_value_type, df.fieldtype)) { - frappe.throw( - __( - "'In Global Search' is not allowed for field {0} of type {1}", - get_field_data(df) - ) - ); - } - }); - }, - async save_changes() { - if (!this.dirty) { - frappe.show_alert({ message: __("No changes to save"), indicator: "orange" }); - return; + function get_updated_fields() { + let fields = []; + let idx = 0; + + let layout_fields = JSON.parse(JSON.stringify(layout.value.tabs)); + + layout_fields.forEach((tab, i) => { + if ( + (i == 0 && is_df_updated(tab.df, get_df("Tab Break", "", __("Details")))) || + i > 0 + ) { + idx++; + tab.df.idx = idx; + fields.push(tab.df); } - frappe.dom.freeze(__("Saving...")); + tab.sections.forEach((section, j) => { + // data before section is added + let fields_copy = JSON.parse(JSON.stringify(fields)); + let old_idx = idx; + section.has_fields = false; - try { - if (this.is_customize_form) { - let doc = frappe.get_doc("Customize Form"); - doc.doc_type = this.doctype; - doc.fields = this.get_updated_fields(); - this.validate_fields(doc.fields, doc.istable); - await frappe.call({ method: "save_customization", doc }); - } else { - this.doc.fields = this.get_updated_fields(); - this.validate_fields(this.doc.fields, this.doc.istable); - await frappe.call({ - method: "frappe.desk.form.save.savedocs", - args: { doc: this.doc, action: "Save" }, - }); - } - this.fetch(); - } catch (e) { - console.error(e); - } finally { - frappe.dom.unfreeze(); - } - }, - get_updated_fields() { - let fields = []; - let idx = 0; - - let layout_fields = JSON.parse(JSON.stringify(this.layout.tabs)); - - layout_fields.forEach((tab, i) => { - if ( - (i == 0 && - this.is_df_updated(tab.df, this.get_df("Tab Break", "", __("Details")))) || - i > 0 - ) { + // do not consider first section if label is not set + if ((j == 0 && is_df_updated(section.df, get_df("Section Break"))) || j > 0) { idx++; - tab.df.idx = idx; - fields.push(tab.df); + section.df.idx = idx; + fields.push(section.df); } - tab.sections.forEach((section, j) => { - // data before section is added - let fields_copy = JSON.parse(JSON.stringify(fields)); - let old_idx = idx; - section.has_fields = false; - - // do not consider first section if label is not set + section.columns.forEach((column, k) => { + // do not consider first column if label is not set if ( - (j == 0 && this.is_df_updated(section.df, this.get_df("Section Break"))) || - j > 0 + (k == 0 && is_df_updated(column.df, get_df("Column Break"))) || + k > 0 || + column.fields.length == 0 ) { idx++; - section.df.idx = idx; - fields.push(section.df); + column.df.idx = idx; + fields.push(column.df); } - section.columns.forEach((column, k) => { - // do not consider first column if label is not set - if ( - (k == 0 && - this.is_df_updated(column.df, this.get_df("Column Break"))) || - k > 0 || - column.fields.length == 0 - ) { - idx++; - column.df.idx = idx; - fields.push(column.df); - } - - column.fields.forEach((field) => { - idx++; - field.df.idx = idx; - fields.push(field.df); - section.has_fields = true; - }); + column.fields.forEach((field) => { + idx++; + field.df.idx = idx; + fields.push(field.df); + section.has_fields = true; }); - - // restore data back to data before section is added. - if (!section.has_fields) { - fields = fields_copy || []; - idx = old_idx; - } }); - }); - return fields; - }, - is_df_updated(df, new_df) { - delete df.name; - delete new_df.name; - return JSON.stringify(df) != JSON.stringify(new_df); - }, - get_layout() { - return create_layout(this.doc.fields); - }, - }, + // restore data back to data before section is added. + if (!section.has_fields) { + fields = fields_copy || []; + idx = old_idx; + } + }); + }); + + return fields; + } + + function is_df_updated(df, new_df) { + delete df.name; + delete new_df.name; + return JSON.stringify(df) != JSON.stringify(new_df); + } + + function get_layout() { + return create_layout(doc.value.fields); + } + + return { + doctype, + doc, + layout, + active_tab, + selected_field, + dirty, + read_only, + is_customize_form, + preview, + drag, + get_animation, + selected, + get_docfields, + get_df, + has_standard_field, + current_tab, + fetch, + reset_changes, + validate_fields, + save_changes, + get_updated_fields, + is_df_updated, + get_layout, + }; }); From 28bba3c1885ce818d2c39fccd358a36d4f3b346f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 20 Feb 2023 13:20:41 +0530 Subject: [PATCH 12/43] fix: always use layout from store --- .../public/js/form_builder/components/Tabs.vue | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/form_builder/components/Tabs.vue b/frappe/public/js/form_builder/components/Tabs.vue index 625ca38745..3cf40865c4 100644 --- a/frappe/public/js/form_builder/components/Tabs.vue +++ b/frappe/public/js/form_builder/components/Tabs.vue @@ -9,9 +9,8 @@ import { ref, computed, nextTick } from "vue"; let store = useStore(); let dragged = ref(false); -let layout = computed(() => store.layout); -let has_tabs = computed(() => layout.value.tabs.length > 1); -store.active_tab = layout.value.tabs[0].df.name; +let has_tabs = computed(() => store.layout.tabs.length > 1); +store.active_tab = store.layout.tabs[0].df.name; function activate_tab(tab) { store.active_tab = tab.df.name; @@ -35,11 +34,11 @@ function drag_over(tab) { function add_new_tab() { let tab = { - df: store.get_df("Tab Break", "", "Tab " + (layout.value.tabs.length + 1)), + df: store.get_df("Tab Break", "", "Tab " + (store.layout.tabs.length + 1)), sections: [section_boilerplate()], }; - layout.value.tabs.push(tab); + store.layout.tabs.push(tab); activate_tab(tab); } @@ -77,7 +76,7 @@ function remove_tab() { } function delete_tab(with_children) { - let tabs = layout.value.tabs; + let tabs = store.layout.tabs; let index = tabs.indexOf(store.current_tab); if (!with_children) { @@ -109,11 +108,11 @@ function delete_tab(with_children) {