mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Implement JSON-friendly expression system for events
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
322
JSON_EXPRESSION_SYSTEM.md
Normal file
322
JSON_EXPRESSION_SYSTEM.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# JSON Expression System
|
||||
|
||||
This document describes the JSON-friendly expression system for handling events without requiring external TypeScript functions.
|
||||
|
||||
## Overview
|
||||
|
||||
The JSON Expression System allows you to define dynamic behaviors entirely within JSON schemas, eliminating the need for external compute functions. This makes schemas more portable and easier to edit.
|
||||
|
||||
## Expression Types
|
||||
|
||||
### 1. Simple Expressions
|
||||
|
||||
Use the `expression` field to evaluate dynamic values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "username",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Expression Patterns:**
|
||||
|
||||
- **Data Access**: `"data.fieldName"`, `"data.user.name"`, `"data.items.0.id"`
|
||||
- Access any field in the data context
|
||||
- Supports nested objects using dot notation
|
||||
|
||||
- **Event Access**: `"event.target.value"`, `"event.key"`, `"event.type"`
|
||||
- Access event properties
|
||||
- Commonly used for form inputs
|
||||
|
||||
- **Date Operations**: `"Date.now()"`
|
||||
- Get current timestamp
|
||||
- Useful for creating unique IDs
|
||||
|
||||
- **Literals**: `42`, `"hello"`, `true`, `false`, `null`
|
||||
- Direct values
|
||||
|
||||
### 2. Value Templates
|
||||
|
||||
Use the `valueTemplate` field to create objects with dynamic values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false,
|
||||
"createdBy": "data.currentUser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Template Behavior:**
|
||||
- String values starting with `"data."` or `"event."` are evaluated as expressions
|
||||
- Other values are used as-is
|
||||
- Perfect for creating new objects with dynamic fields
|
||||
|
||||
### 3. Static Values
|
||||
|
||||
Use the `value` field for static values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "isLoading",
|
||||
"value": false
|
||||
}
|
||||
```
|
||||
|
||||
## Action Types with Expression Support
|
||||
|
||||
### set-value
|
||||
Update a data source with a new value.
|
||||
|
||||
**With Expression:**
|
||||
```json
|
||||
{
|
||||
"id": "update-filter",
|
||||
"type": "set-value",
|
||||
"target": "searchQuery",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
**With Static Value:**
|
||||
```json
|
||||
{
|
||||
"id": "reset-filter",
|
||||
"type": "set-value",
|
||||
"target": "searchQuery",
|
||||
"value": ""
|
||||
}
|
||||
```
|
||||
|
||||
### create
|
||||
Add a new item to an array data source.
|
||||
|
||||
**With Value Template:**
|
||||
```json
|
||||
{
|
||||
"id": "add-todo",
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### update
|
||||
Update an existing value (similar to set-value).
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "update-count",
|
||||
"type": "update",
|
||||
"target": "viewCount",
|
||||
"expression": "data.viewCount + 1"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Arithmetic expressions are not yet supported. Use `increment` action type instead.
|
||||
|
||||
### delete
|
||||
Remove an item from an array.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "remove-todo",
|
||||
"type": "delete",
|
||||
"target": "todos",
|
||||
"path": "id",
|
||||
"expression": "data.selectedId"
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Input Field Updates
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "name-input",
|
||||
"type": "Input",
|
||||
"bindings": {
|
||||
"value": { "source": "userName" }
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"event": "change",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "userName",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Creating Objects with IDs
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "create",
|
||||
"target": "items",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"name": "data.newItemName",
|
||||
"status": "pending",
|
||||
"createdAt": "Date.now()"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Resetting Forms
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "formField1",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "formField2",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Success Notifications
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "show-toast",
|
||||
"message": "Item saved successfully!",
|
||||
"variant": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The system maintains backward compatibility with the legacy `compute` function approach:
|
||||
|
||||
**Legacy (still supported):**
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "userName",
|
||||
"compute": "updateUserName"
|
||||
}
|
||||
```
|
||||
|
||||
**New (preferred):**
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "userName",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
The schema loader will automatically hydrate legacy `compute` references while new schemas can use pure JSON expressions.
|
||||
|
||||
## Limitations
|
||||
|
||||
Current limitations (may be addressed in future updates):
|
||||
|
||||
1. **No Arithmetic**: Cannot do `"data.count + 1"` - use `increment` action type instead
|
||||
2. **No String Concatenation**: Cannot do `"Hello " + data.name` - use template strings in future
|
||||
3. **No Complex Logic**: Cannot do nested conditionals or loops
|
||||
4. **No Custom Functions**: Cannot call user-defined functions
|
||||
|
||||
For complex logic, you can still use the legacy `compute` functions or create custom action types.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Compute Functions to Expressions
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
// In compute-functions.ts
|
||||
export const updateNewTodo = (data: any, event: any) => event.target.value
|
||||
|
||||
// In schema
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"compute": "updateNewTodo"
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
// In compute-functions.ts
|
||||
export const computeAddTodo = (data: any) => ({
|
||||
id: Date.now(),
|
||||
text: data.newTodo,
|
||||
completed: false,
|
||||
})
|
||||
|
||||
// In schema
|
||||
{
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"compute": "computeAddTodo"
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the example schemas:
|
||||
- `/src/schemas/todo-list-json.json` - Pure JSON event system example
|
||||
- `/src/schemas/todo-list.json` - Legacy compute function approach
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned features for future versions:
|
||||
|
||||
1. **Arithmetic Expressions**: `"data.count + 1"`
|
||||
2. **String Templates**: `"Hello ${data.userName}"`
|
||||
3. **Comparison Operators**: `"data.age > 18"`
|
||||
4. **Logical Operators**: `"data.isActive && data.isVerified"`
|
||||
5. **Array Operations**: `"data.items.filter(...)"`, `"data.items.map(...)"`
|
||||
6. **String Methods**: `"data.text.trim()"`, `"data.email.toLowerCase()"`
|
||||
|
||||
For now, use the legacy `compute` functions for these complex scenarios.
|
||||
@@ -1,24 +1,53 @@
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Action, JSONUIContext } from '@/types/json-ui'
|
||||
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||
|
||||
export function useActionExecutor(context: JSONUIContext) {
|
||||
const { data, updateData, executeAction: contextExecute } = context
|
||||
|
||||
const executeAction = useCallback(async (action: Action, event?: any) => {
|
||||
try {
|
||||
const evaluationContext = { data, event }
|
||||
|
||||
switch (action.type) {
|
||||
case 'create': {
|
||||
if (!action.target) return
|
||||
const currentData = data[action.target] || []
|
||||
const newValue = action.compute ? action.compute(data, event) : action.value
|
||||
|
||||
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': {
|
||||
if (!action.target) return
|
||||
const newValue = action.compute ? action.compute(data, event) : action.value
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
updateData(action.target, newValue)
|
||||
break
|
||||
}
|
||||
@@ -38,7 +67,18 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
|
||||
case 'set-value': {
|
||||
if (!action.target) return
|
||||
const newValue = action.compute ? action.compute(data, event) : action.value
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
updateData(action.target, newValue)
|
||||
break
|
||||
}
|
||||
|
||||
192
src/lib/json-ui/expression-evaluator.ts
Normal file
192
src/lib/json-ui/expression-evaluator.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* JSON-friendly expression evaluator
|
||||
* Safely evaluates simple expressions without requiring external functions
|
||||
*/
|
||||
|
||||
interface EvaluationContext {
|
||||
data: Record<string, any>
|
||||
event?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely evaluate a JSON expression
|
||||
* Supports:
|
||||
* - Data access: "data.fieldName", "data.user.name"
|
||||
* - Event access: "event.target.value", "event.key"
|
||||
* - Literals: numbers, strings, booleans, null
|
||||
* - Date operations: "Date.now()"
|
||||
* - Basic operations: trim(), toLowerCase(), toUpperCase()
|
||||
*/
|
||||
export function evaluateExpression(
|
||||
expression: string | undefined,
|
||||
context: EvaluationContext
|
||||
): any {
|
||||
if (!expression) return undefined
|
||||
|
||||
const { data, event } = context
|
||||
|
||||
try {
|
||||
// Handle direct data access: "data.fieldName"
|
||||
if (expression.startsWith('data.')) {
|
||||
return getNestedValue(data, expression.substring(5))
|
||||
}
|
||||
|
||||
// Handle event access: "event.target.value"
|
||||
if (expression.startsWith('event.')) {
|
||||
return getNestedValue(event, expression.substring(6))
|
||||
}
|
||||
|
||||
// Handle Date.now()
|
||||
if (expression === 'Date.now()') {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
// Handle string literals
|
||||
if (expression.startsWith('"') && expression.endsWith('"')) {
|
||||
return expression.slice(1, -1)
|
||||
}
|
||||
if (expression.startsWith("'") && expression.endsWith("'")) {
|
||||
return expression.slice(1, -1)
|
||||
}
|
||||
|
||||
// Handle numbers
|
||||
const num = Number(expression)
|
||||
if (!isNaN(num)) {
|
||||
return num
|
||||
}
|
||||
|
||||
// Handle booleans
|
||||
if (expression === 'true') return true
|
||||
if (expression === 'false') return false
|
||||
if (expression === 'null') return null
|
||||
if (expression === 'undefined') return undefined
|
||||
|
||||
// If no pattern matched, return the expression as-is
|
||||
console.warn(`Expression "${expression}" could not be evaluated, returning as-is`)
|
||||
return expression
|
||||
} catch (error) {
|
||||
console.error(`Failed to evaluate expression "${expression}":`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value from object using dot notation
|
||||
* Example: getNestedValue({ user: { name: 'John' } }, 'user.name') => 'John'
|
||||
*/
|
||||
function getNestedValue(obj: any, path: string): any {
|
||||
if (!obj || !path) return undefined
|
||||
|
||||
const parts = path.split('.')
|
||||
let current = obj
|
||||
|
||||
for (const part of parts) {
|
||||
if (current == null) return undefined
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply string operation to a value
|
||||
* Supports: trim, toLowerCase, toUpperCase, length
|
||||
*/
|
||||
export function applyStringOperation(value: any, operation: string): any {
|
||||
if (value == null) return value
|
||||
|
||||
const str = String(value)
|
||||
|
||||
switch (operation) {
|
||||
case 'trim':
|
||||
return str.trim()
|
||||
case 'toLowerCase':
|
||||
return str.toLowerCase()
|
||||
case 'toUpperCase':
|
||||
return str.toUpperCase()
|
||||
case 'length':
|
||||
return str.length
|
||||
default:
|
||||
console.warn(`Unknown string operation: ${operation}`)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a template object with dynamic values
|
||||
* Example: { "id": "Date.now()", "text": "data.newTodo" }
|
||||
*/
|
||||
export function evaluateTemplate(
|
||||
template: Record<string, any>,
|
||||
context: EvaluationContext
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(template)) {
|
||||
if (typeof value === 'string') {
|
||||
result[key] = evaluateExpression(value, context)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a condition expression
|
||||
* Supports:
|
||||
* - "data.field > 0"
|
||||
* - "data.field.length > 0"
|
||||
* - "data.field === 'value'"
|
||||
* - "data.field != null"
|
||||
*/
|
||||
export function evaluateCondition(
|
||||
condition: string | undefined,
|
||||
context: EvaluationContext
|
||||
): boolean {
|
||||
if (!condition) return true
|
||||
|
||||
const { data } = context
|
||||
|
||||
try {
|
||||
// Simple pattern matching for common conditions
|
||||
// "data.field > 0"
|
||||
const gtMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*>\s*(.+)$/)
|
||||
if (gtMatch) {
|
||||
const value = getNestedValue(data, gtMatch[1])
|
||||
const threshold = Number(gtMatch[2])
|
||||
return (value ?? 0) > threshold
|
||||
}
|
||||
|
||||
// "data.field.length > 0"
|
||||
const lengthMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\.length\s*>\s*(.+)$/)
|
||||
if (lengthMatch) {
|
||||
const value = getNestedValue(data, lengthMatch[1])
|
||||
const threshold = Number(lengthMatch[2])
|
||||
const length = value?.length ?? 0
|
||||
return length > threshold
|
||||
}
|
||||
|
||||
// "data.field === 'value'"
|
||||
const eqMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*===\s*['"](.+)['"]$/)
|
||||
if (eqMatch) {
|
||||
const value = getNestedValue(data, eqMatch[1])
|
||||
return value === eqMatch[2]
|
||||
}
|
||||
|
||||
// "data.field != null"
|
||||
const nullCheck = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*!=\s*null$/)
|
||||
if (nullCheck) {
|
||||
const value = getNestedValue(data, nullCheck[1])
|
||||
return value != null
|
||||
}
|
||||
|
||||
// If no pattern matched, log warning and return true (fail open)
|
||||
console.warn(`Condition "${condition}" could not be evaluated, defaulting to true`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Failed to evaluate condition "${condition}":`, error)
|
||||
return true // Fail open
|
||||
}
|
||||
}
|
||||
150
src/schemas/todo-list-json.json
Normal file
150
src/schemas/todo-list-json.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"id": "todo-list-json",
|
||||
"name": "Todo List (Pure JSON)",
|
||||
"layout": {
|
||||
"type": "single"
|
||||
},
|
||||
"dataSources": [
|
||||
{
|
||||
"id": "todos",
|
||||
"type": "kv",
|
||||
"key": "app-todos-json",
|
||||
"defaultValue": [
|
||||
{ "id": 1, "text": "Learn JSON-driven UI", "completed": true },
|
||||
{ "id": 2, "text": "Build with pure JSON events", "completed": false },
|
||||
{ "id": 3, "text": "No TypeScript functions needed!", "completed": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "newTodo",
|
||||
"type": "static",
|
||||
"defaultValue": ""
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "root",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "h-full overflow-auto p-6 bg-gradient-to-br from-background via-background to-primary/5"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header",
|
||||
"type": "div",
|
||||
"props": { "className": "mb-6" },
|
||||
"children": [
|
||||
{
|
||||
"id": "title",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent",
|
||||
"children": "Pure JSON Todo List"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "subtitle",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground",
|
||||
"children": "No TypeScript functions required! All events use JSON expressions."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "input-row",
|
||||
"type": "div",
|
||||
"props": { "className": "flex gap-2 mb-6 max-w-xl" },
|
||||
"children": [
|
||||
{
|
||||
"id": "todo-input",
|
||||
"type": "Input",
|
||||
"props": { "placeholder": "Add a new task..." },
|
||||
"bindings": {
|
||||
"value": { "source": "newTodo" }
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"event": "change",
|
||||
"actions": [
|
||||
{
|
||||
"id": "update-input",
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"event": "keyPress",
|
||||
"actions": [
|
||||
{
|
||||
"id": "add-on-enter",
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clear-input-after-add",
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "add-button",
|
||||
"type": "Button",
|
||||
"props": { "children": "Add Task" },
|
||||
"events": [
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
{
|
||||
"id": "add-todo",
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clear-input",
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"id": "show-success",
|
||||
"type": "show-toast",
|
||||
"message": "Task added successfully!",
|
||||
"variant": "success"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "info-text",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-sm text-muted-foreground mb-4",
|
||||
"children": "✨ This entire page uses pure JSON expressions - no TypeScript compute functions!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalActions": []
|
||||
}
|
||||
@@ -35,7 +35,12 @@ export interface Action {
|
||||
target?: string
|
||||
path?: string
|
||||
value?: any
|
||||
// Legacy: function-based compute
|
||||
compute?: (data: Record<string, any>, event?: any) => any
|
||||
// New: JSON-friendly expression (e.g., "event.target.value", "data.fieldName")
|
||||
expression?: string
|
||||
// New: JSON template with dynamic values
|
||||
valueTemplate?: Record<string, any>
|
||||
message?: string
|
||||
variant?: 'success' | 'error' | 'info' | 'warning'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user