383 lines
11 KiB
JavaScript
383 lines
11 KiB
JavaScript
import { globalStore } from '@/stores/global'
|
|
import { getMeta } from '@/stores/meta'
|
|
import { call, createListResource, toast } 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 || {}
|
|
}
|
|
},
|
|
onError: (err) => {
|
|
console.error(
|
|
`Error loading CRM Form Scripts for ${doctype} (view: ${view}):`,
|
|
err,
|
|
)
|
|
},
|
|
})
|
|
|
|
if (!doctypeScripts[doctype] && !scripts.loading) {
|
|
scripts.fetch()
|
|
}
|
|
|
|
async function setupScript(document, helpers = {}) {
|
|
await scripts.list.promise
|
|
|
|
let scriptDefs = doctypeScripts[doctype]
|
|
if (!scriptDefs || Object.keys(scriptDefs).length === 0) return null
|
|
|
|
const { $dialog, $socket, makeCall } = globalStore()
|
|
|
|
helpers.createDialog = $dialog
|
|
helpers.toast = toast
|
|
helpers.socket = $socket
|
|
helpers.router = router
|
|
helpers.call = call
|
|
|
|
helpers.crm = {
|
|
makePhoneCall: makeCall,
|
|
}
|
|
|
|
return setupMultipleFormControllers(scriptDefs, 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,
|
|
) {
|
|
document.actions = document.actions || []
|
|
document.statuses = document.statuses || []
|
|
|
|
let instance = new FormClass()
|
|
|
|
// Store the original document context to be used by properties like 'actions'
|
|
instance._originalDocumentContext = document
|
|
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]
|
|
}
|
|
|
|
const getDoc = () => document.doc
|
|
|
|
if (isChildDoctype) {
|
|
instance.doc = createDocProxy(getDoc, parentInstance, instance)
|
|
|
|
if (!parentInstance._childInstances) {
|
|
parentInstance._childInstances = []
|
|
}
|
|
|
|
parentInstance._childInstances.push(instance)
|
|
} else {
|
|
instance.doc = createDocProxy(getDoc, instance)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
function setupHelperMethods(FormClass) {
|
|
if (typeof FormClass.prototype.getRow !== 'function') {
|
|
FormClass.prototype.getRow = function (parentField, idx) {
|
|
idx = idx || this.currentRowIdx
|
|
|
|
let dt = null
|
|
|
|
if (this instanceof Array) {
|
|
const { getFields } = getMeta(this.doc.doctype)
|
|
let fields = getFields()
|
|
let field = fields.find((f) => f.fieldname === parentField)
|
|
dt = field?.options?.replace(/\s+/g, '')
|
|
|
|
if (!idx && dt) {
|
|
idx = this.find((r) => r.constructor.name === dt)?.currentRowIdx
|
|
}
|
|
}
|
|
|
|
if (!this.doc[parentField]) {
|
|
console.warn(
|
|
__('⚠️ No data found for parent field: {0}', [parentField]),
|
|
)
|
|
return null
|
|
}
|
|
const row = this.doc[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 || this.doc.name
|
|
|
|
if (this instanceof Array && dt) {
|
|
return createDocProxy(
|
|
row,
|
|
this.find((r) => r.constructor.name === dt),
|
|
)
|
|
}
|
|
|
|
return createDocProxy(row, this)
|
|
}
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(FormClass.prototype, 'actions')) {
|
|
Object.defineProperty(FormClass.prototype, 'actions', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
if (!this._originalDocumentContext) {
|
|
console.warn(
|
|
'CRM Script: _originalDocumentContext not found on instance for actions getter.',
|
|
)
|
|
return []
|
|
}
|
|
|
|
return this._originalDocumentContext.actions
|
|
},
|
|
set(newValue) {
|
|
if (!this._originalDocumentContext) {
|
|
console.warn(
|
|
'CRM Script: _originalDocumentContext not found on instance for actions setter.',
|
|
)
|
|
return
|
|
}
|
|
if (!Array.isArray(newValue)) {
|
|
console.warn(
|
|
'CRM Script: "actions" property must be an array. Value was not set.',
|
|
newValue,
|
|
)
|
|
this._originalDocumentContext.actions = []
|
|
return
|
|
}
|
|
this._originalDocumentContext.actions = newValue
|
|
},
|
|
})
|
|
}
|
|
|
|
if (
|
|
!Object.prototype.hasOwnProperty.call(FormClass.prototype, 'statuses')
|
|
) {
|
|
Object.defineProperty(FormClass.prototype, 'statuses', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
if (!this._originalDocumentContext) {
|
|
console.warn(
|
|
'CRM Script: _originalDocumentContext not found on instance for statuses getter.',
|
|
)
|
|
return []
|
|
}
|
|
|
|
return this._originalDocumentContext.statuses
|
|
},
|
|
set(newValue) {
|
|
if (!this._originalDocumentContext) {
|
|
console.warn(
|
|
'CRM Script: _originalDocumentContext not found on instance for statuses setter.',
|
|
)
|
|
return
|
|
}
|
|
if (!Array.isArray(newValue)) {
|
|
console.warn(
|
|
'CRM Script: "statuses" property must be an array. Value was not set.',
|
|
newValue,
|
|
)
|
|
this._originalDocumentContext.statuses = []
|
|
return
|
|
}
|
|
this._originalDocumentContext.statuses = newValue
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
|
|
setupHelperMethods(FormClass)
|
|
|
|
return FormClass
|
|
}
|
|
|
|
function createDocProxy(source, instance, childInstance = null) {
|
|
const isFunction = typeof source === 'function'
|
|
const getCurrentData = () => (isFunction ? source() : source)
|
|
|
|
return new Proxy(
|
|
{},
|
|
{
|
|
get(target, prop) {
|
|
const currentDocData = getCurrentData()
|
|
if (!currentDocData) return undefined
|
|
|
|
if (prop === 'trigger') {
|
|
if (currentDocData && 'trigger' in currentDocData) {
|
|
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 currentDocData[prop]
|
|
},
|
|
set(target, prop, value) {
|
|
const currentDocData = getCurrentData()
|
|
if (!currentDocData) return false
|
|
|
|
currentDocData[prop] = value
|
|
return true
|
|
},
|
|
has(target, prop) {
|
|
const currentDocData = getCurrentData()
|
|
if (!currentDocData) return false
|
|
return prop in currentDocData
|
|
},
|
|
ownKeys(target) {
|
|
const currentDocData = getCurrentData()
|
|
if (!currentDocData) return []
|
|
return Reflect.ownKeys(currentDocData)
|
|
},
|
|
getOwnPropertyDescriptor(target, prop) {
|
|
const currentDocData = getCurrentData()
|
|
if (!currentDocData) return undefined
|
|
return Reflect.getOwnPropertyDescriptor(currentDocData, prop)
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
return {
|
|
scripts,
|
|
setupScript,
|
|
setupFormController,
|
|
}
|
|
}
|