From b03abdd2eb523e17b62c34a5e2610387480c3847 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:22:26 +0530 Subject: [PATCH 01/41] fix: get scripts api --- crm/api/script.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 crm/api/script.py diff --git a/crm/api/script.py b/crm/api/script.py new file mode 100644 index 00000000..bfbfa405 --- /dev/null +++ b/crm/api/script.py @@ -0,0 +1,17 @@ +import frappe +from pypika import Criterion + + +@frappe.whitelist() +def get_scripts(doctype, view="Form"): + Script = frappe.qb.DocType("CRM Form Script") + query = ( + frappe.qb.from_(Script) + .select("*") + .where(Script.dt == doctype) + .where(Script.view == view) + .where(Script.enabled == 1) + ) + + scripts = query.run(as_dict=True) + return scripts From 6da3761e76a5a8bbd2f3059b04790c5c73645c76 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:27:00 +0530 Subject: [PATCH 02/41] fix: check if setupForm exist --- frontend/src/utils/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 289e9120..47c3e590 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -152,6 +152,7 @@ export function setupAssignees(doc) { } async function getFormScript(script, obj) { + if (!script.includes('setupForm(')) return {} let scriptFn = new Function(script + '\nreturn setupForm')() let formScript = await scriptFn(obj) return formScript || {} From 7b34c5eb66509851e993df19335a5ebedd0f3055 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:52:11 +0530 Subject: [PATCH 03/41] fix: load script and setup class instances --- crm/api/script.py | 17 ------ frontend/src/data/script.js | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 17 deletions(-) delete mode 100644 crm/api/script.py create mode 100644 frontend/src/data/script.js diff --git a/crm/api/script.py b/crm/api/script.py deleted file mode 100644 index bfbfa405..00000000 --- a/crm/api/script.py +++ /dev/null @@ -1,17 +0,0 @@ -import frappe -from pypika import Criterion - - -@frappe.whitelist() -def get_scripts(doctype, view="Form"): - Script = frappe.qb.DocType("CRM Form Script") - query = ( - frappe.qb.from_(Script) - .select("*") - .where(Script.dt == doctype) - .where(Script.view == view) - .where(Script.enabled == 1) - ) - - scripts = query.run(as_dict=True) - return scripts diff --git a/frontend/src/data/script.js b/frontend/src/data/script.js new file mode 100644 index 00000000..3464ecfe --- /dev/null +++ b/frontend/src/data/script.js @@ -0,0 +1,110 @@ +import { globalStore } from '@/stores/global' +import { createToast } from '@/utils' +import { call, createListResource } from 'frappe-ui' +import { reactive } from 'vue' +import router from '@/router' + +const doctypeScripts = reactive({}) + +export function getScript(doctype, view = 'Form') { + const scripts = createListResource({ + doctype: 'CRM Form Script', + cache: ['Form Scripts', doctype, view], + fields: ['name', 'dt', 'view', 'script'], + filters: { view, dt: doctype, enabled: 1 }, + onSuccess: (_scripts) => { + for (let script of _scripts) { + if (!doctypeScripts[doctype]) { + doctypeScripts[doctype] = {} + } + doctypeScripts[doctype][script.name] = script || {} + } + }, + }) + + if (!doctypeScripts[doctype] && !scripts.loading) { + scripts.fetch() + } + + function setupScript(document, helpers = {}) { + let scripts = doctypeScripts[doctype] + if (!scripts) return null + + const { $dialog, $socket, makeCall } = globalStore() + + helpers.createDialog = $dialog + helpers.createToast = createToast + helpers.socket = $socket + helpers.router = router + helpers.call = call + + helpers.crm = { + makePhoneCall: makeCall, + } + + return setupMultipleFormControllers(scripts, document, helpers) + } + + function setupMultipleFormControllers(scriptStrings, document, helpers) { + const controllers = {} + + for (let scriptName in scriptStrings) { + let script = scriptStrings[scriptName]?.script + if (!script) continue + try { + const className = getClassName(script) + if (!className) throw new Error('No class found') + + const FormClass = evaluateFormClass(script, className, helpers) + controllers[className] = setupFormController(FormClass, document) + } catch (err) { + console.error('Failed to load form controller:', err) + } + } + + return controllers + } + + function setupFormController(FormClass, document) { + const controller = new FormClass() + + for (const key in document) { + if (document.hasOwnProperty(key)) { + controller[key] = document[key] + } + } + + controller.actions = (controller.actions || []).filter( + (action) => typeof action.condition !== 'function' || action.condition(), + ) + + return controller + } + + // utility function to setup a form controller + function getClassName(script) { + const match = script.match(/class\s+([A-Za-z0-9_]+)/) + return match ? match[1] : null + } + + function evaluateFormClass(script, className, helpers = {}) { + const helperKeys = Object.keys(helpers) + const helperValues = Object.values(helpers) + + const wrappedScript = ` + ${script} + return ${className}; + ` + + const FormClass = new Function(...helperKeys, wrappedScript)( + ...helperValues, + ) + return FormClass + } + + return { + scripts, + setupScript, + setupFormController, + } +} From ccd240f4e8a5c717ed3d3929c1b710714ad54527 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:54:13 +0530 Subject: [PATCH 04/41] fix: created document composable to get any doctype record --- frontend/src/data/document.js | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/src/data/document.js diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js new file mode 100644 index 00000000..052023b5 --- /dev/null +++ b/frontend/src/data/document.js @@ -0,0 +1,36 @@ +import { createToast } from '@/utils' +import { createDocumentResource } from 'frappe-ui' + +const documentsCache = {} + +export function useDocument(doctype, docname) { + documentsCache[doctype] = documentsCache[doctype] || {} + + if (!documentsCache[doctype][docname]) { + documentsCache[doctype][docname] = createDocumentResource({ + doctype: doctype, + name: docname, + setValue: { + onSuccess: () => { + createToast({ + title: 'Data Updated', + icon: 'check', + iconClasses: 'text-ink-green-3', + }) + }, + onError: (err) => { + createToast({ + title: 'Error', + text: err.messages[0], + icon: 'x', + iconClasses: 'text-red-600', + }) + }, + }, + }) + } + + return { + document: documentsCache[doctype][docname], + } +} From 1e2f325c55e05bc711f8b97a1d96fd5c7316b0f4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:55:54 +0530 Subject: [PATCH 05/41] fix: setup form script in document.js --- frontend/src/data/document.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js index 052023b5..884d0876 100644 --- a/frontend/src/data/document.js +++ b/frontend/src/data/document.js @@ -1,15 +1,19 @@ +import { getScript } from '@/data/script' import { createToast } from '@/utils' import { createDocumentResource } from 'frappe-ui' const documentsCache = {} export function useDocument(doctype, docname) { + const { setupScript } = getScript(doctype) + documentsCache[doctype] = documentsCache[doctype] || {} if (!documentsCache[doctype][docname]) { documentsCache[doctype][docname] = createDocumentResource({ doctype: doctype, name: docname, + onSuccess: () => setupFormScript(), setValue: { onSuccess: () => { createToast({ @@ -30,7 +34,20 @@ export function useDocument(doctype, docname) { }) } + function setupFormScript() { + const controllers = setupScript(documentsCache[doctype][docname]) + const doctypeName = doctype.replace(/\s+/g, '') + const doctypeController = controllers[doctypeName] + + if (!doctypeController) return + + documentsCache[doctype][docname]['controller'] = doctypeController + } + return { document: documentsCache[doctype][docname], + getActions: () => + documentsCache[doctype][docname]?.controller?.actions || [], + setupFormScript, } } From 16a3f3d66c3eec4c2e9ded6ae1468002409107ad Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:56:18 +0530 Subject: [PATCH 06/41] fix: created triggerOnChange method --- frontend/src/data/document.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/data/document.js b/frontend/src/data/document.js index 884d0876..0c2f96a3 100644 --- a/frontend/src/data/document.js +++ b/frontend/src/data/document.js @@ -44,10 +44,30 @@ export function useDocument(doctype, docname) { documentsCache[doctype][docname]['controller'] = doctypeController } + async function triggerOnChange(fieldname) { + if (!documentsCache[doctype][docname]?.controller) return + + const c = documentsCache[doctype][docname].controller + c.oldValue = getOldValue(fieldname) + c.value = documentsCache[doctype][docname].doc[fieldname] + + return await c[fieldname]?.() + } + + function getOldValue(fieldname) { + if (!documentsCache[doctype][docname]) return '' + + const document = documentsCache[doctype][docname] + const oldDoc = document.originalDoc + return oldDoc?.[fieldname] || document.doc[fieldname] + } + return { document: documentsCache[doctype][docname], getActions: () => documentsCache[doctype][docname]?.controller?.actions || [], + getOldValue, + triggerOnChange, setupFormScript, } } From e65899e38406452cef0c7ec879038037facda2ed Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 17:57:17 +0530 Subject: [PATCH 07/41] fix: use document to load doc data in DataFields --- .../src/components/Activities/DataFields.vue | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/Activities/DataFields.vue b/frontend/src/components/Activities/DataFields.vue index e73fb891..f572bbc0 100644 --- a/frontend/src/components/Activities/DataFields.vue +++ b/frontend/src/components/Activities/DataFields.vue @@ -5,7 +5,7 @@
{{ __('Data') }}
@@ -38,7 +38,7 @@
@@ -49,7 +49,7 @@ @reload=" () => { tabs.reload() - data.reload() + document.reload() } " /> @@ -59,10 +59,10 @@ import EditIcon from '@/components/Icons/EditIcon.vue' import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue' import FieldLayout from '@/components/FieldLayout/FieldLayout.vue' -import { Badge, createResource, createDocumentResource } from 'frappe-ui' +import { Badge, createResource } from 'frappe-ui' import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue' -import { createToast } from '@/utils' import { usersStore } from '@/stores/users' +import { useDocument } from '@/data/document' import { isMobileView } from '@/composables/settings' import { ref, watch } from 'vue' @@ -76,33 +76,11 @@ const props = defineProps({ required: true, }, }) - const { isManager } = usersStore() const showDataFieldsModal = ref(false) -const data = createDocumentResource({ - doctype: props.doctype, - name: props.docname, - setValue: { - onSuccess: () => { - data.reload() - createToast({ - title: 'Data Updated', - icon: 'check', - iconClasses: 'text-ink-green-3', - }) - }, - onError: (err) => { - createToast({ - title: 'Error', - text: err.messages[0], - icon: 'x', - iconClasses: 'text-red-600', - }) - }, - }, -}) +const { document } = useDocument(props.doctype, props.docname) const tabs = createResource({ url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', @@ -112,19 +90,19 @@ const tabs = createResource({ }) function saveChanges() { - data.save.submit() + document.save.submit() } watch( - () => data.doc, + () => document.doc, (newValue, oldValue) => { if (!oldValue) return if (newValue && oldValue) { const isDirty = - JSON.stringify(newValue) !== JSON.stringify(data.originalDoc) - data.isDirty = isDirty + JSON.stringify(newValue) !== JSON.stringify(document.originalDoc) + document.isDirty = isDirty if (isDirty) { - data.save.loading = false + document.save.loading = false } } }, From a30503ca5fbebb87c05cdf5b794eca0c9ce3491e Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 1 May 2025 18:01:18 +0530 Subject: [PATCH 08/41] fix: use document to load doc data in sidepanel layout --- frontend/src/components/SidePanelLayout.vue | 98 ++++++++++++--------- frontend/src/pages/Deal.vue | 2 +- frontend/src/pages/Lead.vue | 2 +- 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/SidePanelLayout.vue b/frontend/src/components/SidePanelLayout.vue index 71bcf973..e84aeb15 100644 --- a/frontend/src/components/SidePanelLayout.vue +++ b/frontend/src/components/SidePanelLayout.vue @@ -55,7 +55,10 @@ > -
+
@@ -67,21 +70,21 @@ class="flex h-7 cursor-pointer items-center px-2 py-1 text-ink-gray-5" > -
{{ data[field.fieldname] }}
+
{{ document.doc[field.fieldname] }}