mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
255 lines
8.0 KiB
TypeScript
255 lines
8.0 KiB
TypeScript
import { useCallback } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { Action, JSONUIContext } from '@/types/json-ui'
|
|
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
|
import { getNestedValue } from '@/lib/json-ui/utils'
|
|
|
|
export function useActionExecutor(context: JSONUIContext) {
|
|
const { data, updateData, updatePath, executeAction: contextExecute } = context
|
|
|
|
const getTargetParts = (target?: string) => {
|
|
if (!target) return null
|
|
const [sourceId, ...pathParts] = target.split('.')
|
|
const path = pathParts.join('.')
|
|
return { sourceId, path: path || undefined }
|
|
}
|
|
|
|
const executeAction = useCallback(async (action: Action, event?: any) => {
|
|
try {
|
|
const evaluationContext = { data, event }
|
|
const updateByPath = (sourceId: string, path: string, value: any) => {
|
|
if (updatePath) {
|
|
updatePath(sourceId, path, value)
|
|
return
|
|
}
|
|
|
|
const sourceData = data[sourceId] ?? {}
|
|
const pathParts = path.split('.')
|
|
const newData = { ...sourceData }
|
|
let current: any = newData
|
|
|
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
const key = pathParts[i]
|
|
current[key] = typeof current[key] === 'object' && current[key] !== null ? { ...current[key] } : {}
|
|
current = current[key]
|
|
}
|
|
|
|
current[pathParts[pathParts.length - 1]] = value
|
|
updateData(sourceId, newData)
|
|
}
|
|
|
|
const resolveDialogTarget = () => {
|
|
const defaultSourceId = 'uiState'
|
|
const hasExplicitTarget = Boolean(action.target && action.path)
|
|
const sourceId = hasExplicitTarget ? action.target : defaultSourceId
|
|
const dialogId = action.path ?? action.target
|
|
|
|
if (!dialogId) return null
|
|
|
|
const dialogPath = dialogId.startsWith('dialogs.') ? dialogId : `dialogs.${dialogId}`
|
|
return { sourceId, dialogPath }
|
|
}
|
|
|
|
switch (action.type) {
|
|
case 'create': {
|
|
if (!action.target) return
|
|
const currentData = data[action.target] || []
|
|
|
|
let newValue
|
|
if (action.compute) {
|
|
// Legacy: compute function
|
|
newValue = action.compute(data, event)
|
|
} else if (action.expression) {
|
|
// New: JSON expression
|
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
|
} else if (action.valueTemplate) {
|
|
// New: JSON template with dynamic values
|
|
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
|
} else {
|
|
// Fallback: static value
|
|
newValue = action.value
|
|
}
|
|
|
|
updateData(action.target, [...currentData, newValue])
|
|
break
|
|
}
|
|
|
|
case 'update': {
|
|
const targetParts = getTargetParts(action.target)
|
|
if (!targetParts) return
|
|
|
|
let newValue
|
|
if (action.compute) {
|
|
newValue = action.compute(data, event)
|
|
} else if (action.expression) {
|
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
|
} else if (action.valueTemplate) {
|
|
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
|
} else {
|
|
newValue = action.value
|
|
}
|
|
|
|
if (targetParts.path) {
|
|
updatePath(targetParts.sourceId, targetParts.path, newValue)
|
|
} else {
|
|
updateData(targetParts.sourceId, newValue)
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'delete': {
|
|
if (!action.target || !action.value) return
|
|
const currentData = data[action.target] || []
|
|
const filtered = currentData.filter((item: any) => {
|
|
if (action.path) {
|
|
return item[action.path] !== action.value
|
|
}
|
|
return item !== action.value
|
|
})
|
|
updateData(action.target, filtered)
|
|
break
|
|
}
|
|
|
|
case 'set-value': {
|
|
const targetParts = getTargetParts(action.target)
|
|
if (!targetParts) return
|
|
|
|
let newValue
|
|
if (action.compute) {
|
|
newValue = action.compute(data, event)
|
|
} else if (action.expression) {
|
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
|
} else if (action.valueTemplate) {
|
|
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
|
} else {
|
|
newValue = action.value
|
|
}
|
|
|
|
if (targetParts.path) {
|
|
updatePath(targetParts.sourceId, targetParts.path, newValue)
|
|
} else {
|
|
updateData(targetParts.sourceId, newValue)
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'toggle-value': {
|
|
const targetParts = getTargetParts(action.target)
|
|
if (!targetParts) return
|
|
|
|
const currentValue = targetParts.path
|
|
? getNestedValue(data[targetParts.sourceId], targetParts.path)
|
|
: data[targetParts.sourceId]
|
|
const nextValue = !currentValue
|
|
|
|
if (targetParts.path) {
|
|
updatePath(targetParts.sourceId, targetParts.path, nextValue)
|
|
} else {
|
|
updateData(targetParts.sourceId, nextValue)
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'increment': {
|
|
const targetParts = getTargetParts(action.target)
|
|
if (!targetParts) return
|
|
|
|
const currentValue = targetParts.path
|
|
? getNestedValue(data[targetParts.sourceId], targetParts.path)
|
|
: data[targetParts.sourceId]
|
|
const amount = action.value || 1
|
|
const nextValue = (currentValue || 0) + amount
|
|
|
|
if (targetParts.path) {
|
|
updatePath(targetParts.sourceId, targetParts.path, nextValue)
|
|
} else {
|
|
updateData(targetParts.sourceId, nextValue)
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'decrement': {
|
|
const targetParts = getTargetParts(action.target)
|
|
if (!targetParts) return
|
|
|
|
const currentValue = targetParts.path
|
|
? getNestedValue(data[targetParts.sourceId], targetParts.path)
|
|
: data[targetParts.sourceId]
|
|
const amount = action.value || 1
|
|
const nextValue = (currentValue || 0) - amount
|
|
|
|
if (targetParts.path) {
|
|
updatePath(targetParts.sourceId, targetParts.path, nextValue)
|
|
} else {
|
|
updateData(targetParts.sourceId, nextValue)
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'show-toast': {
|
|
const message = action.message || 'Action completed'
|
|
const variant = action.variant || 'success'
|
|
|
|
switch (variant) {
|
|
case 'success':
|
|
toast.success(message)
|
|
break
|
|
case 'error':
|
|
toast.error(message)
|
|
break
|
|
case 'info':
|
|
toast.info(message)
|
|
break
|
|
case 'warning':
|
|
toast.warning(message)
|
|
break
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'navigate': {
|
|
if (action.path) {
|
|
window.location.hash = action.path
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'open-dialog': {
|
|
const dialogTarget = resolveDialogTarget()
|
|
if (!dialogTarget) return
|
|
updateByPath(dialogTarget.sourceId, dialogTarget.dialogPath, true)
|
|
break
|
|
}
|
|
|
|
case 'close-dialog': {
|
|
const dialogTarget = resolveDialogTarget()
|
|
if (!dialogTarget) return
|
|
updateByPath(dialogTarget.sourceId, dialogTarget.dialogPath, false)
|
|
break
|
|
}
|
|
|
|
case 'custom': {
|
|
if (contextExecute) {
|
|
await contextExecute(action, event)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Action execution failed:', error)
|
|
toast.error('Action failed')
|
|
}
|
|
}, [data, updateData, updatePath, contextExecute])
|
|
|
|
const executeActions = useCallback(async (actions: Action[], event?: any) => {
|
|
for (const action of actions) {
|
|
await executeAction(action, event)
|
|
}
|
|
}, [executeAction])
|
|
|
|
return {
|
|
executeAction,
|
|
executeActions,
|
|
}
|
|
}
|