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 classNames = getClassNames(script) if (!classNames) continue classNames.forEach((className) => { if (!className) { if (script.includes('setupForm(')) { let message = __( 'setupForm() is deprecated, use class syntax instead. Check the documentation for more details.', ) createToast({ title: __('Deprecation Warning'), text: message, icon: 'alert-triangle', iconClasses: 'text-orange-500', timeout: 10, }) console.warn(message) } throw new Error(__('No class found in script')) } const FormClass = evaluateFormClass(script, className, helpers) if (!FormClass) return let parentInstance = null let doctypeName = doctype.replace(/\s+/g, '') // if className is not doctype name, then it is a child doctype let isChildDoctype = className !== doctypeName if (isChildDoctype) { parentInstance = controllers[doctypeName] } controllers[className] = setupFormController( FormClass, document, parentInstance, isChildDoctype, ) }) } catch (err) { console.error('Failed to load form controller:', err) } } return controllers } function setupFormController( FormClass, document, parentInstance = null, isChildDoctype = false, ) { let instance = new FormClass() for (const key in document) { if (document.hasOwnProperty(key)) { instance[key] = document[key] } } if (isChildDoctype) { setupHelperMethods(FormClass, instance, document) instance.doc = createDocProxy(document.doc, parentInstance) } else { instance.doc = createDocProxy(document.doc, instance) } instance.actions = (instance.actions || []).filter( (action) => typeof action.condition !== 'function' || action.condition(), ) return instance } function setupHelperMethods(FormClass, instance, document) { FormClass.prototype.getRow = (parentField, idx) => getRow(parentField, idx, document.doc, instance) exposeHiddenMethods(document.doc, instance, ['getRow']) } function getRow(parentField, idx, data, instance) { idx = idx || instance.currentRowIdx if (!data[parentField]) { console.warn(`⚠️ No data found for parent field: ${parentField}`) return null } const row = data[parentField].find((r) => r.idx === idx) if (!row) { console.warn( `⚠️ No row found for idx: ${idx} in parent field: ${parentField}`, ) return null } return createDocProxy(row, instance) } // utility function to setup a form controller function getClassNames(script) { return ( [...script.matchAll(/class\s+([A-Za-z0-9_]+)/g)].map( (match) => match[1], ) || [] ) } 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 } function createDocProxy(data, instance) { return new Proxy(data, { get(target, prop) { if (prop === 'trigger') { if ('trigger' in data) { console.warn( `⚠️ Avoid using "trigger" as a field name — it conflicts with the built-in trigger() method.`, ) } return (methodName, ...args) => { const method = instance[methodName] if (typeof method === 'function') { return method.apply(instance, args) } else { console.warn(`⚠️ Method "${methodName}" not found in class.`) } } } return target[prop] }, set(target, prop, value) { target[prop] = value return true }, }) } function exposeHiddenMethods(doc, instance, methodNames = []) { for (const name of methodNames) { if (typeof instance[name] === 'function') { // Show as actual method on doc, bound to instance Object.defineProperty(doc, name, { value: (...args) => instance[name](...args), writable: false, enumerable: false, configurable: false, }) } } } return { scripts, setupScript, setupFormController, } }