Merge pull request #154 from johndoe6345789/codex/update-json-ui-context-and-path-handling

Support dotted action targets for nested data updates
This commit is contained in:
2026-01-18 13:35:13 +00:00
committed by GitHub
5 changed files with 84 additions and 29 deletions

View File

@@ -96,6 +96,20 @@ Update a data source with a new value.
}
```
**Target-path convention:**
To update nested values inside a data source, use a dotted `target` where the prefix is the data source ID and the remainder is the nested path:
```json
{
"id": "set-city",
"type": "set-value",
"target": "profile.address.city",
"expression": "event.target.value"
}
```
This dotted `target` format works with `set-value`, `update`, `toggle-value`, `increment`, and `decrement`.
### create
Add a new item to an array data source.

View File

@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from 'react'
import { useKV } from '@/hooks/use-kv'
import { DataSource } from '@/types/json-ui'
import { setNestedValue } from '@/lib/json-ui/utils'
export function useDataSources(dataSources: DataSource[]) {
const [data, setData] = useState<Record<string, any>>({})
@@ -86,18 +87,8 @@ export function useDataSources(dataSources: DataSource[]) {
return prev
}
const pathParts = path.split('.')
const newData = { ...sourceData }
let current: any = newData
for (let i = 0; i < pathParts.length - 1; i++) {
if (!(pathParts[i] in current)) {
current[pathParts[i]] = {}
}
current = current[pathParts[i]]
}
current[pathParts[pathParts.length - 1]] = value
const newData = Array.isArray(sourceData) ? [...sourceData] : { ...sourceData }
setNestedValue(newData, path, value)
if (source.type === 'kv') {
const kvIndex = kvSources.indexOf(source)

View File

@@ -2,9 +2,17 @@ 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, executeAction: contextExecute } = context
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 {
@@ -35,7 +43,8 @@ export function useActionExecutor(context: JSONUIContext) {
}
case 'update': {
if (!action.target) return
const targetParts = getTargetParts(action.target)
if (!targetParts) return
let newValue
if (action.compute) {
@@ -47,8 +56,12 @@ export function useActionExecutor(context: JSONUIContext) {
} else {
newValue = action.value
}
updateData(action.target, newValue)
if (targetParts.path) {
updatePath(targetParts.sourceId, targetParts.path, newValue)
} else {
updateData(targetParts.sourceId, newValue)
}
break
}
@@ -66,7 +79,8 @@ export function useActionExecutor(context: JSONUIContext) {
}
case 'set-value': {
if (!action.target) return
const targetParts = getTargetParts(action.target)
if (!targetParts) return
let newValue
if (action.compute) {
@@ -78,31 +92,65 @@ export function useActionExecutor(context: JSONUIContext) {
} else {
newValue = action.value
}
updateData(action.target, newValue)
if (targetParts.path) {
updatePath(targetParts.sourceId, targetParts.path, newValue)
} else {
updateData(targetParts.sourceId, newValue)
}
break
}
case 'toggle-value': {
if (!action.target) return
const currentValue = data[action.target]
updateData(action.target, !currentValue)
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': {
if (!action.target) return
const currentValue = data[action.target] || 0
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
updateData(action.target, currentValue + amount)
const nextValue = (currentValue || 0) + amount
if (targetParts.path) {
updatePath(targetParts.sourceId, targetParts.path, nextValue)
} else {
updateData(targetParts.sourceId, nextValue)
}
break
}
case 'decrement': {
if (!action.target) return
const currentValue = data[action.target] || 0
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
updateData(action.target, currentValue - amount)
const nextValue = (currentValue || 0) - amount
if (targetParts.path) {
updatePath(targetParts.sourceId, targetParts.path, nextValue)
} else {
updateData(targetParts.sourceId, nextValue)
}
break
}
@@ -145,7 +193,7 @@ export function useActionExecutor(context: JSONUIContext) {
console.error('Action execution failed:', error)
toast.error('Action failed')
}
}, [data, updateData, contextExecute])
}, [data, updateData, updatePath, contextExecute])
const executeActions = useCallback(async (actions: Action[], event?: any) => {
for (const action of actions) {

View File

@@ -17,6 +17,7 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
const context = {
data,
updateData,
updatePath,
executeAction: onCustomAction || (async () => {}),
}

View File

@@ -147,6 +147,7 @@ export interface PageSchema {
export interface JSONUIContext {
data: Record<string, any>
updateData: (sourceId: string, value: any) => void
updatePath: (sourceId: string, path: string, value: any) => void
executeAction: (action: Action, event?: any) => Promise<void>
}