Update JSON UI actions and conditionals

This commit is contained in:
2026-01-18 16:19:42 +00:00
parent f547d38539
commit bbcc91dc80
12 changed files with 84 additions and 72 deletions

View File

@@ -27,6 +27,7 @@ Use the `expression` field to evaluate dynamic values:
- Supports nested objects using dot notation
- **Event Access**: `"event.target.value"`, `"event.key"`, `"event.type"`
- You can also reference the full event payload with `"event"`
- Access event properties
- Commonly used for form inputs
@@ -110,6 +111,20 @@ To update nested values inside a data source, use a dotted `target` where the pr
This dotted `target` format works with `set-value`, `update`, `toggle-value`, `increment`, and `decrement`.
## Conditional Expressions
Conditional rendering uses `conditional.if` strings that are evaluated against the current data context:
```json
{
"conditional": {
"if": "statusFilter === 'running'"
}
}
```
Legacy conditional objects (with `source`/`operator`/`value`) should be migrated to these inline expressions so schemas stay compatible with the JSON UI renderer.
### create
Add a new item to an array data source.

View File

@@ -106,7 +106,12 @@ Converted three complex pages (Models, Component Trees, and Workflows) from trad
"type": "Component",
"bindings": { "prop": { "source": "...", "path": "..." } },
"events": [
{ "event": "click", "actions": [...] }
{
"event": "click",
"actions": [
{ "type": "set-value", "target": "selectedId", "expression": "event" }
]
}
]
}
]
@@ -115,6 +120,18 @@ Converted three complex pages (Models, Component Trees, and Workflows) from trad
}
```
### Action & Conditional Syntax
- Use supported JSON UI action types (for example, `set-value`, `toggle-value`, `show-toast`) with `target`, `path`, `value`, or `expression` fields instead of legacy `setState` actions.
- Replace legacy conditional objects (`{ "source": "...", "operator": "eq|gt|truthy|falsy", "value": ... }`) with `conditional.if` expressions:
```json
{
"conditional": {
"if": "modelCount === 0"
}
}
```
### Component Registry Integration
All JSON page wrappers are registered in `component-registry.ts`:
- `JSONModelDesigner`

View File

@@ -117,7 +117,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createBlueprintDialogOpen",
"value": true
}
@@ -150,9 +150,7 @@
"className": "p-4 space-y-2"
},
"conditional": {
"source": "blueprintCount",
"operator": "gt",
"value": 0
"if": "blueprintCount > 0"
},
"children": [
{
@@ -165,11 +163,7 @@
"className": "group cursor-pointer hover:bg-accent/10 transition-all duration-200 hover:shadow-lg hover:shadow-accent/20 border-2 hover:border-accent/50"
},
"conditionalClass": {
"condition": {
"source": "selectedBlueprintId",
"operator": "eq",
"valueFrom": "item.id"
},
"condition": "selectedBlueprintId === item.id",
"trueClass": "bg-accent/20 border-accent shadow-md shadow-accent/30",
"falseClass": "border-transparent"
},
@@ -178,9 +172,9 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "selectedBlueprintId",
"valueFrom": "item.id"
"expression": "event.item.id"
}
]
}
@@ -330,9 +324,7 @@
"className": "flex flex-col items-center justify-center h-full p-8 text-center"
},
"conditional": {
"source": "blueprintCount",
"operator": "eq",
"value": 0
"if": "blueprintCount === 0"
},
"children": [
{
@@ -367,7 +359,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createBlueprintDialogOpen",
"value": true
}
@@ -404,8 +396,7 @@
"className": "flex-1 overflow-auto p-8"
},
"conditional": {
"source": "selectedBlueprint",
"operator": "truthy"
"if": "selectedBlueprint"
},
"children": [
{
@@ -467,7 +458,7 @@
"event": "onClick",
"actions": [
{
"type": "toast",
"type": "show-toast",
"message": "Edit blueprint coming soon",
"variant": "info"
}
@@ -496,7 +487,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createEndpointDialogOpen",
"value": true
}
@@ -662,9 +653,7 @@
"className": "space-y-3"
},
"conditional": {
"source": "endpointCount",
"operator": "gt",
"value": 0
"if": "endpointCount > 0"
},
"children": [
{
@@ -743,7 +732,7 @@
"event": "onClick",
"actions": [
{
"type": "toast",
"type": "show-toast",
"message": "Edit endpoint coming soon",
"variant": "info"
}
@@ -804,9 +793,7 @@
"className": "text-center py-12"
},
"conditional": {
"source": "endpointCount",
"operator": "eq",
"value": 0
"if": "endpointCount === 0"
},
"children": [
{
@@ -835,7 +822,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createEndpointDialogOpen",
"value": true
}
@@ -869,8 +856,7 @@
"className": "flex-1 flex items-center justify-center p-8"
},
"conditional": {
"source": "selectedBlueprint",
"operator": "falsy"
"if": "!selectedBlueprint"
},
"children": [
{

View File

@@ -96,7 +96,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createDialogOpen",
"value": true
}
@@ -128,9 +128,7 @@
"className": "space-y-2"
},
"conditional": {
"source": "lambdaCount",
"operator": "gt",
"value": 0
"if": "lambdaCount > 0"
},
"children": [
{
@@ -145,9 +143,7 @@
"className": "flex flex-col items-center justify-center py-12 px-4 text-center"
},
"conditional": {
"source": "lambdaCount",
"operator": "eq",
"value": 0
"if": "lambdaCount === 0"
},
"children": [
{
@@ -191,8 +187,7 @@
"className": "flex-1 flex items-center justify-center p-8"
},
"conditional": {
"source": "selectedLambda",
"operator": "truthy"
"if": "selectedLambda"
},
"children": [
{
@@ -248,8 +243,7 @@
"className": "flex-1 flex items-center justify-center p-8"
},
"conditional": {
"source": "selectedLambda",
"operator": "falsy"
"if": "!selectedLambda"
},
"children": [
{

View File

@@ -20,9 +20,7 @@
"className": "text-xs"
},
"conditional": {
"source": "badge",
"operator": "neq",
"value": null
"if": "badge !== null"
}
}
]

View File

@@ -33,9 +33,7 @@
"className": "flex items-center gap-1.5 text-xs text-muted-foreground"
},
"conditional": {
"source": "lastSaved",
"operator": "neq",
"value": null
"if": "lastSaved !== null"
},
"children": [
{

View File

@@ -31,9 +31,9 @@
"event": "onChange",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "searchValue",
"valueFrom": "event.target.value"
"expression": "event.target.value"
}
]
}
@@ -48,16 +48,14 @@
"className": "absolute right-1 h-7 w-7 p-0"
},
"conditional": {
"source": "searchValue",
"operator": "neq",
"value": ""
"if": "searchValue !== \"\""
},
"events": [
{
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "searchValue",
"value": ""
}

View File

@@ -314,9 +314,9 @@
"event": "onValueChange",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "selectedTab",
"valueFrom": "event"
"expression": "event"
}
]
}
@@ -571,7 +571,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "customColorDialogOpen",
"value": true
}
@@ -602,9 +602,7 @@
"className": "text-center py-8"
},
"conditional": {
"source": "customColorCount",
"operator": "eq",
"value": 0
"if": "customColorCount === 0"
},
"children": [
{
@@ -630,9 +628,7 @@
"className": "text-sm text-muted-foreground"
},
"conditional": {
"source": "customColorCount",
"operator": "gt",
"value": 0
"if": "customColorCount > 0"
},
"children": "Custom colors will be displayed here"
}

View File

@@ -84,7 +84,7 @@
"event": "onClick",
"actions": [
{
"type": "setState",
"type": "set-value",
"target": "createDialogOpen",
"value": true
}
@@ -131,9 +131,7 @@
{
"type": "div",
"conditional": {
"source": "workflowCount",
"operator": "eq",
"value": 0
"if": "workflowCount === 0"
},
"props": {
"className": "text-center py-8 text-muted-foreground"
@@ -158,8 +156,7 @@
"className": "flex-1 flex items-center justify-center"
},
"conditional": {
"source": "selectedWorkflow",
"operator": "falsy"
"if": "!selectedWorkflow"
},
"children": [
{
@@ -204,8 +201,7 @@
"className": "flex-1 p-6 overflow-auto"
},
"conditional": {
"source": "selectedWorkflow",
"operator": "truthy"
"if": "selectedWorkflow"
},
"children": [
{

View File

@@ -102,7 +102,10 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven
? handler.condition(mergedData as Record<string, any>)
: evaluateCondition(handler.condition, mergedData as Record<string, any>))
if (conditionMet) {
onEvent(component.id, handler, e)
const eventPayload = typeof e === 'object' && e !== null
? Object.assign(e as Record<string, unknown>, context)
: e
onEvent(component.id, handler, eventPayload)
}
}
})

View File

@@ -26,6 +26,14 @@ export function evaluateExpression(
const { data, event } = context
try {
if (expression === 'event') {
return event
}
if (expression === 'data') {
return data
}
// Handle direct data access: "data.fieldName"
if (expression.startsWith('data.')) {
return getNestedValue(data, expression.substring(5))

View File

@@ -157,7 +157,10 @@ export function JSONUIRenderer({
: evaluateCondition(handler.condition, { ...dataMap, ...renderContext })
if (!conditionMet) return
}
onAction?.(handler.actions, event)
const eventPayload = typeof event === 'object' && event !== null
? Object.assign(event, renderContext)
: event
onAction?.(handler.actions, eventPayload)
}
})
}