From 4e8bbd6c93daf3bc9458382050861e2249cbaf6b Mon Sep 17 00:00:00 2001 From: Aradhya Date: Fri, 2 Dec 2022 12:47:17 +0530 Subject: [PATCH 01/63] refactor: allowing unlocking of doc when job id is not set --- .../doctype/submission_queue/submission_queue.js | 2 +- .../doctype/submission_queue/submission_queue.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/submission_queue/submission_queue.js b/frappe/core/doctype/submission_queue/submission_queue.js index 93d6b981dc..414c8c9ee0 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.js +++ b/frappe/core/doctype/submission_queue/submission_queue.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Submission Queue", { refresh: function (frm) { - if (frm.doc.status === "Queued" && frm.doc.job_id) { + if (frm.doc.status === "Queued") { frm.add_custom_button(__("Unlock Reference Document"), () => { frappe.confirm(__("Are you sure you want to go ahead with this action?"), () => { frm.call("unlock_doc"); diff --git a/frappe/core/doctype/submission_queue/submission_queue.py b/frappe/core/doctype/submission_queue/submission_queue.py index 2bb4200a87..caa0352c97 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.py +++ b/frappe/core/doctype/submission_queue/submission_queue.py @@ -69,6 +69,10 @@ class SubmissionQueue(Document): def background_submission(self, to_be_queued_doc: Document, action_for_queuing: str): # Set the job id for that submission doctype + submission_doc = frappe.get_doc(self.doctype, self.name) + if submission_doc.state == "Failed": + # If the document has already been unlocked by _unlock_reference_doc_unlock_reference_doc + return self.update_job_id(get_current_job().id) _action = action_for_queuing.lower() if _action == "update": @@ -129,9 +133,10 @@ class SubmissionQueue(Document): enqueue_create_notification([notify_to], notification_doc) def _unlock_reference_doc(self): - """ - Only execute if self.job_id is defined. - """ + if not self.job_id: + self.queued_doc.unlock() + frappe.db.set_value(self.doctype, self.name, {"status": "Failed"}) + try: job = Job.fetch(self.job_id, connection=get_redis_conn()) status = job.get_status(refresh=True) @@ -156,8 +161,7 @@ class SubmissionQueue(Document): # NOTE: this can lead to some weird unlocking/locking behaviours. # for example: hitting unlock on a submission could lead to unlocking of another submission # of the same reference document. - - if self.status != "Queued" and not self.job_id: + if self.status != "Queued": return self._unlock_reference_doc() From 1b46b0e34768540de855457e9f42331170c755c4 Mon Sep 17 00:00:00 2001 From: Aradhya Date: Fri, 2 Dec 2022 13:54:25 +0530 Subject: [PATCH 02/63] fix: fixed status fetch and refactored message --- .../submission_queue/submission_queue.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/frappe/core/doctype/submission_queue/submission_queue.py b/frappe/core/doctype/submission_queue/submission_queue.py index caa0352c97..3e30ef1ef0 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.py +++ b/frappe/core/doctype/submission_queue/submission_queue.py @@ -68,11 +68,10 @@ class SubmissionQueue(Document): ) def background_submission(self, to_be_queued_doc: Document, action_for_queuing: str): - # Set the job id for that submission doctype - submission_doc = frappe.get_doc(self.doctype, self.name) - if submission_doc.state == "Failed": - # If the document has already been unlocked by _unlock_reference_doc_unlock_reference_doc + if self.status == "Failed": + # If the document has already been unlocked by _unlock_reference_doc return + # Set the job id for that submission doctyp self.update_job_id(get_current_job().id) _action = action_for_queuing.lower() if _action == "update": @@ -97,17 +96,20 @@ class SubmissionQueue(Document): self.notify(values["status"], action_for_queuing) def notify(self, submission_status: str, action: str): + message = _("Action {0} run on {1} {2} ") if submission_status == "Failed": doctype = self.doctype docname = self.name - message = _("Submission of {0} {1} with action {2} failed") + message += "failed" else: doctype = self.ref_doctype docname = self.ref_docname - message = _("Submission of {0} {1} with action {2} completed successfully") + message += "finished" message = message.format( - frappe.bold(str(self.ref_doctype)), frappe.bold(self.ref_docname), frappe.bold(action) + frappe.bold(action), + frappe.bold(str(self.ref_doctype)), + frappe.bold(self.ref_docname), ) time_diff = time_diff_in_seconds(now(), self.created_at) if cint(time_diff) <= 60: @@ -133,10 +135,6 @@ class SubmissionQueue(Document): enqueue_create_notification([notify_to], notification_doc) def _unlock_reference_doc(self): - if not self.job_id: - self.queued_doc.unlock() - frappe.db.set_value(self.doctype, self.name, {"status": "Failed"}) - try: job = Job.fetch(self.job_id, connection=get_redis_conn()) status = job.get_status(refresh=True) @@ -169,7 +167,7 @@ class SubmissionQueue(Document): def queue_submission(doc: Document, action: str, alert: bool = True): queue = frappe.new_doc("Submission Queue") - queue.state = "Queued" + queue.status = "Queued" queue.ref_doctype = doc.doctype queue.ref_docname = doc.name queue.insert(doc, action) From e00d89f430206d338f770c464cd30ff0ab5f4d52 Mon Sep 17 00:00:00 2001 From: Aradhya Date: Fri, 2 Dec 2022 15:06:58 +0530 Subject: [PATCH 03/63] feat: Added queue_submission to workflows --- frappe/model/workflow.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 8338157996..e7835fba8d 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -101,6 +101,9 @@ def is_transition_condition_satisfied(transition, doc) -> bool: @frappe.whitelist() def apply_workflow(doc, action): """Allow workflow action on the current doc""" + from frappe.core.doctype.submission_queue.submission_queue import queue_submission + from frappe.utils.scheduler import is_scheduler_inactive + doc = frappe.get_doc(frappe.parse_json(doc)) workflow = get_workflow(doc.doctype) transitions = get_transitions(doc, workflow) @@ -132,7 +135,10 @@ def apply_workflow(doc, action): if doc.docstatus.is_draft() and new_docstatus == DocStatus.draft(): doc.save() elif doc.docstatus.is_draft() and new_docstatus == DocStatus.submitted(): - doc.submit() + if doc.meta.queue_in_background and not is_scheduler_inactive(): + queue_submission(doc, action="submit") + else: + doc.submit() elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.submitted(): doc.save() elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.cancelled(): From cffcb0fa176eefbfb4bcc754b0621043255646f3 Mon Sep 17 00:00:00 2001 From: Aradhya Date: Sat, 3 Dec 2022 23:40:57 +0530 Subject: [PATCH 04/63] refactor: failed attempts banner --- .../submission_queue/submission_queue.py | 28 ++++++++++++++++--- frappe/public/js/frappe/form/form.js | 10 +++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/submission_queue/submission_queue.py b/frappe/core/doctype/submission_queue/submission_queue.py index 3e30ef1ef0..181f5f21cb 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.py +++ b/frappe/core/doctype/submission_queue/submission_queue.py @@ -182,14 +182,34 @@ def queue_submission(doc: Document, action: str, alert: bool = True): ) +def format_tb(traceback: str): + traceback = traceback.strip().split("\n")[-1] + if len(traceback.split()) > 6: + return " ".join(traceback.split()[0:6]) + "..." + return traceback + + @frappe.whitelist() def get_latest_submissions(doctype, docname): # NOTE: not used creation as orderby intentianlly as we have used update_modified=False everywhere # hence assuming modified will be equal to creation for submission queue documents dt = "Submission Queue" + out = {} + filters = {"ref_doctype": doctype, "ref_docname": docname} - return { - "latest_submission": frappe.db.get_value(dt, filters), - "latest_failed_submission": frappe.db.get_value(dt, filters | {"status": "Failed"}), - } + failed_submission = frappe.db.get_value( + dt, filters=filters | {"status": "Failed"}, fieldname=["name", "exception"] + ) + latest_submission = frappe.db.get_value(dt, filters=filters, fieldname=["name", "status"]) + + if failed_submission: + out["latest_failed_submission"], out["latest_failed_submission_exc_info"] = ( + failed_submission[0], + format_tb(failed_submission[1]), + ) + + if latest_submission: + out["latest_submission"], out["latest_submission_status"] = latest_submission + + return out diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 75a1def1dc..0f9ff22dee 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -2081,18 +2081,22 @@ frappe.ui.form.Form = class FrappeForm { col_width = 3; failed_link = ``; } else { - submission_label = __("Previous Falied Submission"); + if (r.message.latest_failed_submission_exc_info) { + submission_label = r.message.latest_failed_submission_exc_info; + } else { + submission_label = "Errored"; + } } } let html = `
- ${__("Submission Status:")} + ${__(`Submission Status: ${r.message.latest_submission_status}`)}
${submission_label} From 12f0be1906d2cd6d84c9d3759a1909cbed65c178 Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 3 Jan 2023 00:41:37 +0530 Subject: [PATCH 05/63] refactor(minor): better banner and removed unnecessary complexity for unlocking ref document --- .../submission_queue/submission_queue.json | 5 +- .../submission_queue/submission_queue.py | 79 +++++++------------ frappe/public/js/frappe/form/form.js | 69 ++++++---------- frappe/public/scss/common/css_variables.scss | 2 +- 4 files changed, 57 insertions(+), 98 deletions(-) diff --git a/frappe/core/doctype/submission_queue/submission_queue.json b/frappe/core/doctype/submission_queue/submission_queue.json index d1f66ffa13..ce28007e23 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.json +++ b/frappe/core/doctype/submission_queue/submission_queue.json @@ -20,8 +20,9 @@ "fields": [ { "fieldname": "job_id", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Job Id", + "options": "RQ Job", "read_only": 1 }, { @@ -87,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-12 16:48:37.797232", + "modified": "2023-01-02 23:53:55.010001", "modified_by": "Administrator", "module": "Core", "name": "Submission Queue", diff --git a/frappe/core/doctype/submission_queue/submission_queue.py b/frappe/core/doctype/submission_queue/submission_queue.py index 181f5f21cb..b1e20516a8 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.py +++ b/frappe/core/doctype/submission_queue/submission_queue.py @@ -13,7 +13,6 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create from frappe.model.document import Document from frappe.monitor import add_data_to_monitor from frappe.utils import now, time_diff_in_seconds -from frappe.utils.background_jobs import get_redis_conn from frappe.utils.data import cint @@ -39,6 +38,7 @@ class SubmissionQueue(Document): frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) def insert(self, to_be_queued_doc: Document, action: str): + self.status = "Queued" self.to_be_queued_doc = to_be_queued_doc self.action_for_queuing = action super().insert(ignore_permissions=True) @@ -68,11 +68,9 @@ class SubmissionQueue(Document): ) def background_submission(self, to_be_queued_doc: Document, action_for_queuing: str): - if self.status == "Failed": - # If the document has already been unlocked by _unlock_reference_doc - return # Set the job id for that submission doctyp self.update_job_id(get_current_job().id) + _action = action_for_queuing.lower() if _action == "update": _action = "submit" @@ -96,21 +94,21 @@ class SubmissionQueue(Document): self.notify(values["status"], action_for_queuing) def notify(self, submission_status: str, action: str): - message = _("Action {0} run on {1} {2} ") if submission_status == "Failed": doctype = self.doctype docname = self.name - message += "failed" + message = _("Submission of {0} {1} with action {2} failed") else: doctype = self.ref_doctype docname = self.ref_docname - message += "finished" + message = _("Submission of {0} {1} with action {2} completed successfully") message = message.format( - frappe.bold(action), frappe.bold(str(self.ref_doctype)), - frappe.bold(self.ref_docname), + frappe.bold(str(self.ref_docname)), + frappe.bold(action), ) + time_diff = time_diff_in_seconds(now(), self.created_at) if cint(time_diff) <= 60: frappe.publish_realtime( @@ -134,40 +132,21 @@ class SubmissionQueue(Document): notify_to = frappe.db.get_value("User", self.enqueued_by, fieldname="email") enqueue_create_notification([notify_to], notification_doc) - def _unlock_reference_doc(self): - try: - job = Job.fetch(self.job_id, connection=get_redis_conn()) - status = job.get_status(refresh=True) - exc = job.exc_info - except NoSuchJobError: - exc = None - status = "failed" - - if status in ("queued", "started"): - frappe.msgprint(_("Document in queue for execution!")) - return - - self.queued_doc.unlock() - values = ( - {"status": "Finished"} if status == "finished" else {"status": "Failed", "exception": exc} - ) - frappe.db.set_value(self.doctype, self.name, values, update_modified=False) - frappe.msgprint(_("Document Unlocked")) - @frappe.whitelist() def unlock_doc(self): # NOTE: this can lead to some weird unlocking/locking behaviours. # for example: hitting unlock on a submission could lead to unlocking of another submission # of the same reference document. + if self.status != "Queued": return - self._unlock_reference_doc() + self.queued_doc.unlock() + frappe.msgprint(_("Document Unlocked")) def queue_submission(doc: Document, action: str, alert: bool = True): queue = frappe.new_doc("Submission Queue") - queue.status = "Queued" queue.ref_doctype = doc.doctype queue.ref_docname = doc.name queue.insert(doc, action) @@ -182,34 +161,30 @@ def queue_submission(doc: Document, action: str, alert: bool = True): ) -def format_tb(traceback: str): - traceback = traceback.strip().split("\n")[-1] - if len(traceback.split()) > 6: - return " ".join(traceback.split()[0:6]) + "..." - return traceback - - @frappe.whitelist() def get_latest_submissions(doctype, docname): # NOTE: not used creation as orderby intentianlly as we have used update_modified=False everywhere # hence assuming modified will be equal to creation for submission queue documents - dt = "Submission Queue" - out = {} - - filters = {"ref_doctype": doctype, "ref_docname": docname} - failed_submission = frappe.db.get_value( - dt, filters=filters | {"status": "Failed"}, fieldname=["name", "exception"] + latest_submission = frappe.db.get_value( + "Submission Queue", + filters={"ref_doctype": doctype, "ref_docname": docname}, + fieldname=["name", "exception", "status"], ) - latest_submission = frappe.db.get_value(dt, filters=filters, fieldname=["name", "status"]) - - if failed_submission: - out["latest_failed_submission"], out["latest_failed_submission_exc_info"] = ( - failed_submission[0], - format_tb(failed_submission[1]), - ) + out = None if latest_submission: - out["latest_submission"], out["latest_submission_status"] = latest_submission + out = { + "latest_submission": latest_submission[0], + "exc": format_tb(latest_submission[1]), + "status": latest_submission[2], + } return out + + +def format_tb(traceback: str | None = None): + if not traceback: + return + + return traceback.strip().split("\n")[-1] diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 0f9ff22dee..25b53638fe 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -2051,16 +2051,12 @@ frappe.ui.form.Form = class FrappeForm { this.doc.docstatus === 0 ) ) { - if (wrapper.length) { - wrapper.hide(); - wrapper.html(""); - } - + wrapper.length && wrapper.remove(); return; } if (!wrapper.length) { - wrapper = $('
'); + wrapper = $('
'); this.layout.wrapper.prepend(wrapper); } @@ -2070,53 +2066,40 @@ frappe.ui.form.Form = class FrappeForm { args: { doctype: this.doctype, docname: this.docname }, }) .then((r) => { - if (r.message.latest_submission) { + if (r.message?.latest_submission) { // if we are here that means some submission(s) were queued and are in queued/failed state - let col_width = 4; - let failed_link = ""; let submission_label = __("Previous Submission"); + let secondary = ""; + let div_class = "col-md-12"; - if (r.message.latest_failed_submission) { - if (r.message.latest_failed_submission !== r.message.latest_submission) { - col_width = 3; - failed_link = ``; - } else { - if (r.message.latest_failed_submission_exc_info) { - submission_label = r.message.latest_failed_submission_exc_info; - } else { - submission_label = "Errored"; - } - } + if (r.message.exc) { + secondary = `: ${r.message.exc}`; + } else { + div_class = "col-md-6"; + secondary = ` +
+
+ ${__( + "All Submissions" + )} + `; } let html = ` -
-
- ${__(`Submission Status: ${r.message.latest_submission_status}`)} + - - ${failed_link} - -
- `; + `; - wrapper.show(); + wrapper.removeClass("red").removeClass("yellow"); + wrapper.addClass(r.message.status == "Failed" ? "red" : "yellow"); wrapper.html(html); } else { - wrapper.hide(); - wrapper.html(""); + wrapper.remove(); } }); } diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 1914e7479b..cfbcc001b6 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -163,7 +163,7 @@ $input-height: 28px !default; --bg-green: var(--dark-green-50); --bg-yellow: var(--yellow-50); --bg-orange: var(--orange-50); - --bg-red: var(--red-50); + --bg-red: var(--red-100); --bg-gray: var(--gray-200); --bg-light-gray: var(--gray-100); --bg-dark-gray: var(--gray-900); From f6489a6de861317f6ae4ac154814317eb528de14 Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 3 Jan 2023 21:06:45 +0530 Subject: [PATCH 06/63] fix: allow submission queue doc reads from users if theyre owners * only show unlock doc button to system managers --- frappe/core/doctype/submission_queue/submission_queue.js | 2 +- frappe/core/doctype/submission_queue/submission_queue.json | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/submission_queue/submission_queue.js b/frappe/core/doctype/submission_queue/submission_queue.js index 414c8c9ee0..fc1e83ac49 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.js +++ b/frappe/core/doctype/submission_queue/submission_queue.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Submission Queue", { refresh: function (frm) { - if (frm.doc.status === "Queued") { + if (frm.doc.status === "Queued" && frappe.boot.user.roles.includes("System Manager")) { frm.add_custom_button(__("Unlock Reference Document"), () => { frappe.confirm(__("Are you sure you want to go ahead with this action?"), () => { frm.call("unlock_doc"); diff --git a/frappe/core/doctype/submission_queue/submission_queue.json b/frappe/core/doctype/submission_queue/submission_queue.json index ce28007e23..4058276319 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.json +++ b/frappe/core/doctype/submission_queue/submission_queue.json @@ -88,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-01-02 23:53:55.010001", + "modified": "2023-01-03 20:54:40.904584", "modified_by": "Administrator", "module": "Core", "name": "Submission Queue", @@ -103,6 +103,11 @@ "report": 1, "role": "System Manager", "share": 1 + }, + { + "if_owner": 1, + "read": 1, + "role": "All" } ], "sort_field": "modified", From 2e5d53ef6f41113a5ad84dbd37c6670e141c44b4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 18 Jan 2023 14:17:05 +0530 Subject: [PATCH 07/63] fix: if section has one column input field's width should be half --- .../js/form_builder/components/FormBuilder.vue | 12 ++++++++++++ frappe/public/js/form_builder/components/Section.vue | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/form_builder/components/FormBuilder.vue b/frappe/public/js/form_builder/components/FormBuilder.vue index c641643414..1ab8c715ea 100644 --- a/frappe/public/js/form_builder/components/FormBuilder.vue +++ b/frappe/public/js/form_builder/components/FormBuilder.vue @@ -109,6 +109,12 @@ onMounted(() => { box-shadow: var(--card-shadow); background-color: var(--card-bg); + :deep(.section-columns.has-one-column .field) { + input.form-control, .signature-field { + width: calc(50% - 19px); + } + } + :deep(.column-container .field.sortable-chosen) { background-color: var(--bg-light-gray); border-radius: var(--border-radius-sm); @@ -221,6 +227,12 @@ onMounted(() => { .section-columns { margin-top: 8px; + &.has-one-column .field { + input.form-control, .signature-field { + width: calc(50% - 15px); + } + } + .section-columns-container { .column { padding-left: 15px; diff --git a/frappe/public/js/form_builder/components/Section.vue b/frappe/public/js/form_builder/components/Section.vue index d23a599fa6..f1419b7c89 100644 --- a/frappe/public/js/form_builder/components/Section.vue +++ b/frappe/public/js/form_builder/components/Section.vue @@ -130,7 +130,13 @@ function move_sections_to_tab() {
{{ section.df.description }}
-
+
Date: Wed, 18 Jan 2023 20:51:11 +0530 Subject: [PATCH 09/63] fix: on duplicate of standard field create a custom field in customize form --- frappe/public/js/form_builder/components/Field.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/public/js/form_builder/components/Field.vue b/frappe/public/js/form_builder/components/Field.vue index 5a7ce5626f..58c2d85b3b 100644 --- a/frappe/public/js/form_builder/components/Field.vue +++ b/frappe/public/js/form_builder/components/Field.vue @@ -32,6 +32,10 @@ function move_fields_to_column() { function duplicate_field() { let duplicate_field = clone_field(props.field); + if (store.is_customize_form) { + duplicate_field.df.is_custom_field = 1; + } + if (duplicate_field.df.label) { duplicate_field.df.label = duplicate_field.df.label + " Copy"; } From 986cc8d634d519bcf021194009f17cbd47c57912 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 18 Jan 2023 21:30:17 +0530 Subject: [PATCH 10/63] fix: make duplicated field as unsaved field reset creation, modified, modified_by, owner --- frappe/public/js/form_builder/components/Field.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/public/js/form_builder/components/Field.vue b/frappe/public/js/form_builder/components/Field.vue index 58c2d85b3b..cf3e21c310 100644 --- a/frappe/public/js/form_builder/components/Field.vue +++ b/frappe/public/js/form_builder/components/Field.vue @@ -40,6 +40,13 @@ function duplicate_field() { duplicate_field.df.label = duplicate_field.df.label + " Copy"; } duplicate_field.df.fieldname = ""; + duplicate_field.df.__islocal = 1; + duplicate_field.df.__unsaved = 1; + duplicate_field.df.owner = frappe.session.user; + + delete duplicate_field.df.creation; + delete duplicate_field.df.modified; + delete duplicate_field.df.modified_by; // push duplicate_field after props.field in the same column let index = props.column.fields.indexOf(props.field); From 30134a2cc98ce99f0303005c5116a56e7909cb49 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 18 Jan 2023 22:10:55 +0530 Subject: [PATCH 11/63] fix: Correct standard docfield types - `creation` and `modified` are timestamps not dates. - `modified_by` is similar to `owner` so not sure why we can't have this render as link as well --- frappe/public/js/frappe/model/model.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 18acf00ad3..b835989c07 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -112,9 +112,14 @@ $.extend(frappe.model, { { fieldname: "name", fieldtype: "Link", label: __("ID") }, { fieldname: "owner", fieldtype: "Link", label: __("Created By"), options: "User" }, { fieldname: "idx", fieldtype: "Int", label: __("Index") }, - { fieldname: "creation", fieldtype: "Date", label: __("Created On") }, - { fieldname: "modified", fieldtype: "Date", label: __("Last Updated On") }, - { fieldname: "modified_by", fieldtype: "Data", label: __("Last Updated By") }, + { fieldname: "creation", fieldtype: "Datetime", label: __("Created On") }, + { fieldname: "modified", fieldtype: "Datetime", label: __("Last Updated On") }, + { + fieldname: "modified_by", + fieldtype: "Link", + label: __("Last Updated By"), + options: "User", + }, { fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") }, { fieldname: "_liked_by", fieldtype: "Data", label: __("Liked By") }, { fieldname: "_comments", fieldtype: "Text", label: __("Comments") }, From 96afc5ebd99f31f4ea584288425e02ec8e0bad4c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 18 Jan 2023 22:13:22 +0530 Subject: [PATCH 12/63] fix: correct invalid filters - Datetime - equality doesn't make sense because of milliseconds. A separate operator for "date" part can be useful here maybe. - Code - data like filter and remove comparison operators. - Phone - treat like Data - Barcode - treat like data - attach - treat like data - attach image - treat like data - rating - remove invalid operators - password - LOL --- frappe/public/js/frappe/ui/filters/filter.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 261b1dd5ba..96c4fd712d 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -39,13 +39,16 @@ frappe.ui.Filter = class { this.invalid_condition_map = { Date: ["like", "not like"], - Datetime: ["like", "not like"], + Datetime: ["like", "not like", "in", "not in", "=", "!="], Data: ["Between", "Timespan"], Select: ["like", "not like", "Between", "Timespan"], Link: ["Between", "Timespan", ">", "<", ">=", "<="], Currency: ["Between", "Timespan"], Color: ["Between", "Timespan"], Check: this.conditions.map((c) => c[0]).filter((c) => c !== "="), + Code: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], + Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], + Rating: ["like", "not like", "Between", "in", "not in", "Timespan"], }; } @@ -497,10 +500,14 @@ frappe.ui.filter_utils = { "Small Text", "Text Editor", "Code", + "Attach", + "Attach Image", "Markdown Editor", "HTML Editor", "Tag", + "Phone", "Comments", + "Barcode", "Dynamic Link", "Read Only", "Assign", From b3f9e69a6e5749bf15af190e951a200f28777b52 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 18 Jan 2023 22:49:05 +0530 Subject: [PATCH 13/63] fix: use frappe.desk.form.save.savedocs instead of frappe.client.save for saving --- frappe/public/js/form_builder/store.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js index 314e8f5ed7..246956dc94 100644 --- a/frappe/public/js/form_builder/store.js +++ b/frappe/public/js/form_builder/store.js @@ -182,8 +182,10 @@ export const useStore = defineStore("form-builder-store", { } else { this.doc.fields = this.get_updated_fields(); this.validate_fields(this.doc.fields, this.doc.istable); - await frappe.call("frappe.client.save", { doc: this.doc }); - frappe.toast("Fields Table Updated"); + await frappe.call({ + method: "frappe.desk.form.save.savedocs", + args: { doc: this.doc, action: "Save" }, + }); } this.fetch(); } catch (e) { From 3b2cfcad2c9b7073e969302056c804498f6e7804 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 18 Jan 2023 23:51:52 +0530 Subject: [PATCH 14/63] fix: ask if need to delete tab/section/column with or without children --- .../js/form_builder/components/Column.vue | 67 +++++++++++++------ .../js/form_builder/components/Section.vue | 45 +++++++++---- .../js/form_builder/components/Tabs.vue | 63 +++++++++-------- frappe/public/js/form_builder/utils.js | 25 +++++++ 4 files changed, 139 insertions(+), 61 deletions(-) diff --git a/frappe/public/js/form_builder/components/Column.vue b/frappe/public/js/form_builder/components/Column.vue index a8f1f84118..a9c4bf0ea1 100644 --- a/frappe/public/js/form_builder/components/Column.vue +++ b/frappe/public/js/form_builder/components/Column.vue @@ -4,7 +4,7 @@ import Field from "./Field.vue"; import EditableInput from "./EditableInput.vue"; import { ref } from "vue"; import { useStore } from "../store"; -import { move_children_to_parent } from "../utils"; +import { move_children_to_parent, confirm_dialog } from "../utils"; let props = defineProps(["section", "column"]); let store = useStore(); @@ -24,32 +24,61 @@ function remove_column() { if (store.is_customize_form && props.column.df.is_custom_field == 0) { frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); throw "cannot delete standard field"; + } else if (props.column.fields.length == 0 || store.has_standard_field(props.column)) { + delete_column(); + } else { + confirm_dialog( + __("Delete Column", null, "Title of confirmation dialog"), + __("Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.", null, "Confirmation dialog message"), + () => delete_column(), + __("Delete column", null, "Button text"), + () => delete_column(true), + __("Delete entire column with fields", null, "Button text") + ); } +} +function delete_column(with_children) { // move all fields to previous column let columns = props.section.columns; let index = columns.indexOf(props.column); - if (index > 0) { - let prev_column = columns[index - 1]; - prev_column.fields = [...prev_column.fields, ...props.column.fields]; - } else { - if (props.column.fields.length != 0) { - // create a new column if current column has fields and push fields to it - columns.unshift({ - df: store.get_df("Column Break"), - fields: props.column.fields, - is_first: true, - }); - index++; + if (with_children && index == 0 && columns.length == 1) { + if (props.column.fields.length == 0) { + frappe.msgprint(__("Section must have at least one column")); + throw "section must have at least one column"; + } + + columns.unshift({ + df: store.get_df("Column Break"), + fields: [], + is_first: true, + }); + index++; + } + + if (!with_children) { + if (index > 0) { + let prev_column = columns[index - 1]; + prev_column.fields = [...prev_column.fields, ...props.column.fields]; } else { - // set next column as first column - let next_column = columns[index + 1]; - if (next_column) { - next_column.is_first = true; + if (props.column.fields.length == 0) { + // set next column as first column + let next_column = columns[index + 1]; + if (next_column) { + next_column.is_first = true; + } else { + frappe.msgprint(__("Section must have at least one column")); + throw "section must have at least one column"; + } } else { - frappe.msgprint(__("Section must have at least one column")); - throw "section must have at least one column"; + // create a new column if current column has fields and push fields to it + columns.unshift({ + df: store.get_df("Column Break"), + fields: props.column.fields, + is_first: true, + }); + index++; } } } diff --git a/frappe/public/js/form_builder/components/Section.vue b/frappe/public/js/form_builder/components/Section.vue index f1419b7c89..4624c72a38 100644 --- a/frappe/public/js/form_builder/components/Section.vue +++ b/frappe/public/js/form_builder/components/Section.vue @@ -4,7 +4,7 @@ import Column from "./Column.vue"; import EditableInput from "./EditableInput.vue"; import { ref } from "vue"; import { useStore } from "../store"; -import { section_boilerplate, move_children_to_parent } from "../utils"; +import { section_boilerplate, move_children_to_parent, confirm_dialog } from "../utils"; let props = defineProps(["tab", "section"]); let store = useStore(); @@ -27,25 +27,42 @@ function remove_section() { if (store.is_customize_form && props.section.df.is_custom_field == 0) { frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); throw "cannot delete standard field"; + } else if (store.has_standard_field(props.section)) { + delete_section(); + } else if (is_section_empty()) { + delete_section(true); + } else { + confirm_dialog( + __("Delete Section", null, "Title of confirmation dialog"), + __("Are you sure you want to delete the section? All the columns along with fields in the section will be moved to the previous section.", null, "Confirmation dialog message"), + () => delete_section(), + __("Delete section", null, "Button text"), + () => delete_section(true), + __("Delete entire section with columns", null, "Button text") + ); } +} +function delete_section(with_children) { let sections = props.tab.sections; let index = sections.indexOf(props.section); - if (index > 0) { - let prev_section = sections[index - 1]; - if (!is_section_empty()) { - // move all columns from current section to previous section - prev_section.columns = [...prev_section.columns, ...props.section.columns]; + if (!with_children) { + if (index > 0) { + let prev_section = sections[index - 1]; + if (!is_section_empty()) { + // move all columns from current section to previous section + prev_section.columns = [...prev_section.columns, ...props.section.columns]; + } + } else if (index == 0 && !is_section_empty()) { + // create a new section and push columns to it + sections.unshift({ + df: store.get_df("Section Break"), + columns: props.section.columns, + is_first: true, + }); + index++; } - } else if (index == 0 && !is_section_empty()) { - // create a new section and push columns to it - sections.unshift({ - df: store.get_df("Section Break"), - columns: props.section.columns, - is_first: true, - }); - index++; } // remove section diff --git a/frappe/public/js/form_builder/components/Tabs.vue b/frappe/public/js/form_builder/components/Tabs.vue index d2ef939f80..625ca38745 100644 --- a/frappe/public/js/form_builder/components/Tabs.vue +++ b/frappe/public/js/form_builder/components/Tabs.vue @@ -3,7 +3,7 @@ import Section from "./Section.vue"; import EditableInput from "./EditableInput.vue"; import draggable from "vuedraggable"; import { useStore } from "../store"; -import { section_boilerplate } from "../utils"; +import { section_boilerplate, confirm_dialog } from "../utils"; import { ref, computed, nextTick } from "vue"; let store = useStore(); @@ -51,44 +51,51 @@ function add_new_section() { function is_current_tab_empty() { // check if sections have columns and it contains fields - return !store.current_tab.sections.some(section => { - // if section doesnt have fields remove the section - let has_fields = section.columns.some(column => column.fields.length); - - if (!has_fields) { - // remove section if empty - let index = store.current_tab.sections.indexOf(section); - store.current_tab.sections.splice(index, 1); - has_fields = true; - } - - return has_fields; - }); + return !store.current_tab.sections.some( + section => section.columns.some(column => column.fields.length) + ); } function remove_tab() { if (store.is_customize_form && store.current_tab.df.is_custom_field == 0) { frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); throw "cannot delete standard field"; + } else if (store.has_standard_field(store.current_tab)) { + delete_tab(); + } else if (is_current_tab_empty()) { + delete_tab(true); + } else { + confirm_dialog( + __("Delete Tab", null, "Title of confirmation dialog"), + __("Are you sure you want to delete the tab? All the sections along with fields in the tab will be moved to the previous tab.", null, "Confirmation dialog message"), + () => delete_tab(), + __("Delete tab", null, "Button text"), + () => delete_tab(true), + __("Delete entire tab with sections", null, "Button text") + ); } +} +function delete_tab(with_children) { let tabs = layout.value.tabs; let index = tabs.indexOf(store.current_tab); - if (index > 0) { - let prev_tab = tabs[index - 1]; - if (!is_current_tab_empty()) { - // move all sections from current tab to previous tab - prev_tab.sections = [...prev_tab.sections, ...store.current_tab.sections]; + if (!with_children) { + if (index > 0) { + let prev_tab = tabs[index - 1]; + if (!is_current_tab_empty()) { + // move all sections from current tab to previous tab + prev_tab.sections = [...prev_tab.sections, ...store.current_tab.sections]; + } + } else { + // create a new tab and push sections to it + tabs.unshift({ + df: store.get_df("Tab Break", "", __("Details")), + sections: store.current_tab.sections, + is_first: true, + }); + index++; } - } else { - // create a new tab and push sections to it - tabs.unshift({ - df: store.get_df("Tab Break", "", __("Details")), - sections: store.current_tab.sections, - is_first: true, - }); - index++; } // remove tab @@ -185,7 +192,7 @@ function remove_tab() {
-
{{ __("Drag & Drop a section here") }}
+
{{ __("Drag & Drop a section here from another tab") }}
{{ __("OR") }}