crm/frontend/src/data/script.js
2025-05-12 16:37:33 +05:30

273 lines
7.1 KiB
JavaScript

import { globalStore } from '@/stores/global'
import { getMeta } from '@/stores/meta'
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 = []
let parentInstanceIdx = null
for (let scriptName in scriptStrings) {
let script = scriptStrings[scriptName]?.script
if (!script) continue
try {
const classNames = getClassNames(script)
if (!classNames) continue
classNames.forEach((className) => {
const FormClass = evaluateFormClass(script, className, helpers)
if (!FormClass) return
let parentInstance = null
let doctypeName = doctype.replace(/\s+/g, '')
let { doctypeMeta } = getMeta(doctype)
// if className is not doctype name, then it is a child doctype
let isChildDoctype = className !== doctypeName
if (isChildDoctype) {
if (!controllers.length) {
console.error(
__(
'⚠️ No class found for doctype: {0}, it is mandatory to have a class for the parent doctype. it can be empty, but it should be present.',
[doctype],
),
)
return
}
parentInstance = controllers[parentInstanceIdx]
} else {
parentInstanceIdx = controllers.length || 0
}
const instance = setupFormController(
FormClass,
doctypeMeta,
document,
parentInstance,
isChildDoctype,
)
controllers.push(instance)
})
} catch (err) {
console.error(__('Failed to load form controller: {0}', [err]))
}
}
return controllers
}
function setupFormController(
FormClass,
meta,
document,
parentInstance = null,
isChildDoctype = false,
) {
let instance = new FormClass()
instance._isChildDoctype = isChildDoctype
for (const key in document) {
if (document.hasOwnProperty(key)) {
instance[key] = document[key]
}
}
instance.getMeta = async (doctype) => {
if (!meta[doctype]) {
await getMeta(doctype)
return meta[doctype]
}
return meta[doctype]
}
setupHelperMethods(FormClass, document)
if (isChildDoctype) {
instance.doc = createDocProxy(document.doc, parentInstance, instance)
if (!parentInstance._childInstances) {
parentInstance._childInstances = []
}
parentInstance._childInstances.push(instance)
} else {
instance.doc = createDocProxy(document.doc, instance)
}
return instance
}
function setupHelperMethods(FormClass, document) {
if (typeof FormClass.prototype.getRow !== 'function') {
FormClass.prototype.getRow = function (parentField, idx) {
let data = document.doc
idx = idx || this.currentRowIdx
let dt = null
if (this instanceof Array) {
const { getFields } = getMeta(data.doctype)
let fields = getFields()
let field = fields.find((f) => f.fieldname === parentField)
dt = field.options
dt = dt.replace(/\s+/g, '')
if (!idx && dt) {
idx = this.find((r) => r.constructor.name === dt)?.currentRowIdx
}
}
if (!data[parentField]) {
console.warn(
__('⚠️ No data found for parent field: {0}', [parentField]),
)
return null
}
const row = data[parentField].find((r) => r.idx === idx)
if (!row) {
console.warn(
__('⚠️ No row found for idx: {0} in parent field: {1}', [
idx,
parentField,
]),
)
return null
}
row.parent = row.parent || data.name
if (this instanceof Array && dt) {
return createDocProxy(
row,
this.find((r) => r.constructor.name === dt),
)
}
return createDocProxy(row, this)
}
}
}
// utility function to setup a form controller
function getClassNames(script) {
const withoutComments = script
.replace(/\/\/.*$/gm, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
// Match class declarations
return (
[...withoutComments.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, childInstance = null) {
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 "{0}" not found in class.', [methodName]),
)
}
}
}
if (prop === 'getRow') {
return instance.getRow.bind(
childInstance || instance._childInstances || instance,
)
}
return target[prop]
},
set(target, prop, value) {
target[prop] = value
return true
},
})
}
return {
scripts,
setupScript,
setupFormController,
}
}