mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Remove legacy compute schema support
This commit is contained in:
62
JSON_EXPRESSION_SYSTEM.md
Normal file
62
JSON_EXPRESSION_SYSTEM.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# JSON Expression System
|
||||
|
||||
This document describes the supported JSON expression patterns used across JSON UI schemas.
|
||||
Legacy compute functions have been removed in favor of expression strings and value templates.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Expressions
|
||||
|
||||
Expressions are string values that resolve against a data + event context:
|
||||
|
||||
```json
|
||||
{
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
Supported expression patterns:
|
||||
|
||||
- `data` or `event`
|
||||
- Dot access: `data.user.name`, `event.target.value`
|
||||
- Literals: numbers, booleans, `null`, `undefined`, quoted strings
|
||||
- Time: `Date.now()`
|
||||
- Array filtering:
|
||||
- `data.todos.filter(completed === true)`
|
||||
- `data.users.filter(status === 'active').length`
|
||||
|
||||
### Value Templates
|
||||
|
||||
Value templates are JSON objects whose string values are evaluated as expressions:
|
||||
|
||||
```json
|
||||
{
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditions
|
||||
|
||||
Conditions use expression strings that are evaluated against the data context:
|
||||
|
||||
```json
|
||||
{
|
||||
"condition": "data.newTodo.length > 0"
|
||||
}
|
||||
```
|
||||
|
||||
Supported condition patterns:
|
||||
|
||||
- `data.field > 0`
|
||||
- `data.field.length > 0`
|
||||
- `data.field === 'value'`
|
||||
- `data.field != null`
|
||||
|
||||
## Legacy Compute Functions (Removed)
|
||||
|
||||
Schemas should no longer reference function names in `compute`, `transform`, or string-based
|
||||
condition fields. Use `expression` and `valueTemplate` instead.
|
||||
@@ -7,30 +7,9 @@ import { ComponentBindingsCard } from '@/components/data-binding-designer/Compon
|
||||
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
||||
import dataBindingCopy from '@/data/data-binding-designer.json'
|
||||
|
||||
interface SeedDataSource extends Omit<DataSource, 'compute'> {
|
||||
computeId?: string
|
||||
}
|
||||
|
||||
const computeRegistry: Record<string, (data: Record<string, any>) => any> = {
|
||||
displayName: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
|
||||
}
|
||||
|
||||
const buildSeedDataSources = (sources: SeedDataSource[]): DataSource[] => {
|
||||
return sources.map((source) => {
|
||||
if (source.type === 'computed' && source.computeId) {
|
||||
return {
|
||||
...source,
|
||||
compute: computeRegistry[source.computeId],
|
||||
}
|
||||
}
|
||||
|
||||
return source
|
||||
})
|
||||
}
|
||||
|
||||
export function DataBindingDesigner() {
|
||||
const [dataSources, setDataSources] = useState<DataSource[]>(
|
||||
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
|
||||
dataBindingCopy.seed.dataSources as DataSource[],
|
||||
)
|
||||
|
||||
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
||||
|
||||
@@ -6,9 +6,12 @@ import { DataSource } from '@/types/json-ui'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
|
||||
interface ComputedSourceFieldsCopy {
|
||||
computeLabel: string
|
||||
computePlaceholder: string
|
||||
computeHelp: string
|
||||
expressionLabel: string
|
||||
expressionPlaceholder: string
|
||||
expressionHelp: string
|
||||
valueTemplateLabel: string
|
||||
valueTemplatePlaceholder: string
|
||||
valueTemplateHelp: string
|
||||
dependenciesLabel: string
|
||||
availableSourcesLabel: string
|
||||
emptyDependencies: string
|
||||
@@ -38,22 +41,37 @@ export function ComputedSourceFields({
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.computeLabel}</Label>
|
||||
<Label>{copy.expressionLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.compute?.toString() || ''}
|
||||
value={editingSource.expression || ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const fn = new Function('data', `return (${e.target.value})`)()
|
||||
onUpdateField('compute', fn)
|
||||
} catch (err) {
|
||||
// Invalid function
|
||||
}
|
||||
onUpdateField('expression', e.target.value)
|
||||
}}
|
||||
placeholder={copy.computePlaceholder}
|
||||
placeholder={copy.expressionPlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.computeHelp}
|
||||
{copy.expressionHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.valueTemplateLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.valueTemplate ? JSON.stringify(editingSource.valueTemplate, null, 2) : ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const template = JSON.parse(e.target.value)
|
||||
onUpdateField('valueTemplate', template)
|
||||
} catch (err) {
|
||||
// Invalid JSON
|
||||
}
|
||||
}}
|
||||
placeholder={copy.valueTemplatePlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.valueTemplateHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"id": "displayName",
|
||||
"type": "computed",
|
||||
"dependencies": ["userProfile"],
|
||||
"computeId": "displayName"
|
||||
"expression": "data.userProfile.name"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
"valuePlaceholder": "{\"key\": \"value\"}"
|
||||
},
|
||||
"computed": {
|
||||
"computeLabel": "Compute Function",
|
||||
"computePlaceholder": "(data) => data.source1 + data.source2",
|
||||
"computeHelp": "Function that computes the value from other data sources",
|
||||
"expressionLabel": "Expression",
|
||||
"expressionPlaceholder": "data.source1",
|
||||
"expressionHelp": "Expression that computes the value from other data sources",
|
||||
"valueTemplateLabel": "Value Template (JSON)",
|
||||
"valueTemplatePlaceholder": "{\n \"total\": \"data.items.length\"\n}",
|
||||
"valueTemplateHelp": "Template object with expressions for computed fields",
|
||||
"dependenciesLabel": "Dependencies",
|
||||
"availableSourcesLabel": "Available Sources",
|
||||
"emptyDependencies": "No data sources available. Create KV or static sources first."
|
||||
|
||||
@@ -9,7 +9,7 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
||||
id: `ds-${Date.now()}`,
|
||||
type,
|
||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||
...(type === 'computed' && { compute: () => null, dependencies: [] }),
|
||||
...(type === 'computed' && { expression: '', dependencies: [] }),
|
||||
...(type === 'static' && { defaultValue: null }),
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,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'
|
||||
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||
|
||||
export function useDataSources(dataSources: DataSource[]) {
|
||||
const [data, setData] = useState<Record<string, any>>({})
|
||||
@@ -43,14 +44,17 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
||||
|
||||
computedSources.forEach(source => {
|
||||
if (source.compute) {
|
||||
const deps = source.dependencies || []
|
||||
const hasAllDeps = deps.every(dep => dep in data)
|
||||
|
||||
if (hasAllDeps) {
|
||||
const computedValue = source.compute(data)
|
||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
||||
}
|
||||
const deps = source.dependencies || []
|
||||
const hasAllDeps = deps.every(dep => dep in data)
|
||||
|
||||
if (hasAllDeps) {
|
||||
const evaluationContext = { data }
|
||||
const computedValue = source.expression
|
||||
? evaluateExpression(source.expression, evaluationContext)
|
||||
: source.valueTemplate
|
||||
? evaluateTemplate(source.valueTemplate, evaluationContext)
|
||||
: source.defaultValue
|
||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
||||
}
|
||||
})
|
||||
}, [data, dataSources])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||
|
||||
export function useDataSources(dataSources: DataSource[]) {
|
||||
const [data, setData] = useState<Record<string, any>>({})
|
||||
@@ -54,8 +55,17 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
computedSources.forEach((ds) => {
|
||||
if (ds.compute && typeof ds.compute === 'function') {
|
||||
result[ds.id] = ds.compute(data)
|
||||
const evaluationContext = { data }
|
||||
if (ds.expression) {
|
||||
result[ds.id] = evaluateExpression(ds.expression, evaluationContext)
|
||||
return
|
||||
}
|
||||
if (ds.valueTemplate) {
|
||||
result[ds.id] = evaluateTemplate(ds.valueTemplate, evaluationContext)
|
||||
return
|
||||
}
|
||||
if (ds.defaultValue !== undefined) {
|
||||
result[ds.id] = ds.defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useWorkflows } from '../data/use-workflows'
|
||||
import { useLambdas } from '../data/use-lambdas'
|
||||
import { useActions } from './use-actions'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||
|
||||
export function usePage(schema: PageSchema) {
|
||||
const files = useFiles()
|
||||
@@ -46,11 +47,15 @@ export function usePage(schema: PageSchema) {
|
||||
const computed: Record<string, any> = {}
|
||||
|
||||
schema.data.forEach(source => {
|
||||
if (source.type === 'computed' && source.compute) {
|
||||
computed[source.id] = evaluateBindingExpression(source.compute, dataContext, {
|
||||
fallback: undefined,
|
||||
label: `computed data (${source.id})`,
|
||||
})
|
||||
if (source.type === 'computed') {
|
||||
if (source.expression) {
|
||||
computed[source.id] = evaluateBindingExpression(source.expression, dataContext, {
|
||||
fallback: undefined,
|
||||
label: `computed data (${source.id})`,
|
||||
})
|
||||
} else if (source.valueTemplate) {
|
||||
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: dataContext })
|
||||
}
|
||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||
computed[source.id] = source.defaultValue
|
||||
}
|
||||
|
||||
@@ -56,10 +56,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
const currentData = data[action.target] || []
|
||||
|
||||
let newValue
|
||||
if (action.compute) {
|
||||
// Legacy: compute function
|
||||
newValue = action.compute(data, event)
|
||||
} else if (action.expression) {
|
||||
if (action.expression) {
|
||||
// New: JSON expression
|
||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||
} else if (action.valueTemplate) {
|
||||
@@ -79,9 +76,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
if (!targetParts) return
|
||||
|
||||
let newValue
|
||||
if (action.compute) {
|
||||
newValue = action.compute(data, event)
|
||||
} else if (action.expression) {
|
||||
if (action.expression) {
|
||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||
} else if (action.valueTemplate) {
|
||||
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||
@@ -102,9 +97,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
const currentData = data[action.target] || []
|
||||
|
||||
let selectorValue
|
||||
if (action.compute) {
|
||||
selectorValue = action.compute(data, event)
|
||||
} else if (action.expression) {
|
||||
if (action.expression) {
|
||||
selectorValue = evaluateExpression(action.expression, evaluationContext)
|
||||
} else if (action.valueTemplate) {
|
||||
selectorValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||
@@ -129,9 +122,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
if (!targetParts) return
|
||||
|
||||
let newValue
|
||||
if (action.compute) {
|
||||
newValue = action.compute(data, event)
|
||||
} else if (action.expression) {
|
||||
if (action.expression) {
|
||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||
} else if (action.valueTemplate) {
|
||||
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||
|
||||
@@ -99,9 +99,7 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven
|
||||
resolvedEventHandlers.forEach(handler => {
|
||||
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
||||
const conditionMet = !handler.condition
|
||||
|| (typeof handler.condition === 'function'
|
||||
? handler.condition(mergedData as Record<string, any>)
|
||||
: evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event handler condition' }))
|
||||
|| evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event handler condition' })
|
||||
if (conditionMet) {
|
||||
const eventPayload = typeof e === 'object' && e !== null
|
||||
? Object.assign(e as Record<string, unknown>, context)
|
||||
|
||||
@@ -34,6 +34,26 @@ export function evaluateExpression(
|
||||
return data
|
||||
}
|
||||
|
||||
const filterMatch = expression.match(
|
||||
/^data\.([a-zA-Z0-9_.]+)\.filter\(\s*([a-zA-Z0-9_.]+)\s*(===|==|!==|!=)\s*(.+?)\s*\)(?:\.(length))?$/
|
||||
)
|
||||
if (filterMatch) {
|
||||
const [, collectionPath, fieldPath, operator, rawValue, lengthSuffix] = filterMatch
|
||||
const collection = getNestedValue(data, collectionPath)
|
||||
if (!Array.isArray(collection)) {
|
||||
return lengthSuffix ? 0 : []
|
||||
}
|
||||
|
||||
const expectedValue = evaluateExpression(rawValue.trim(), { data, event })
|
||||
const isNegated = operator === '!=' || operator === '!=='
|
||||
const filtered = collection.filter((item) => {
|
||||
const fieldValue = getNestedValue(item, fieldPath)
|
||||
return isNegated ? fieldValue !== expectedValue : fieldValue === expectedValue
|
||||
})
|
||||
|
||||
return lengthSuffix ? filtered.length : filtered
|
||||
}
|
||||
|
||||
// Handle direct data access: "data.fieldName"
|
||||
if (expression.startsWith('data.')) {
|
||||
return getNestedValue(data, expression.substring(5))
|
||||
|
||||
@@ -35,7 +35,6 @@ export const ActionSchema = z.object({
|
||||
path: z.string().optional(),
|
||||
value: z.any().optional(),
|
||||
params: z.record(z.string(), z.any()).optional(),
|
||||
compute: z.any().optional(),
|
||||
expression: z.string().optional(),
|
||||
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||
message: z.string().optional(),
|
||||
@@ -45,14 +44,14 @@ export const ActionSchema = z.object({
|
||||
export const EventHandlerSchema = z.object({
|
||||
event: z.string(),
|
||||
actions: z.array(ActionSchema),
|
||||
condition: z.any().optional(),
|
||||
condition: z.string().optional(),
|
||||
})
|
||||
|
||||
export const JSONEventDefinitionSchema = z.object({
|
||||
action: z.string().optional(),
|
||||
actions: z.array(ActionSchema).optional(),
|
||||
payload: z.record(z.string(), z.any()).optional(),
|
||||
condition: z.any().optional(),
|
||||
condition: z.string().optional(),
|
||||
})
|
||||
|
||||
export const JSONEventMapSchema = z.record(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { evaluateTransformExpression } from './expression-helpers'
|
||||
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
type BindingTransform = string
|
||||
|
||||
interface BindingSourceOptions {
|
||||
state?: Record<string, any>
|
||||
@@ -50,10 +50,6 @@ function applyTransform(value: unknown, transform?: BindingTransform) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof transform === 'function') {
|
||||
return transform(value)
|
||||
}
|
||||
|
||||
return evaluateTransformExpression(transform, value, {}, { label: 'data binding transform' })
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,17 @@
|
||||
{
|
||||
"id": "filteredUsers",
|
||||
"type": "computed",
|
||||
"compute": "computeFilteredUsers",
|
||||
"expression": "data.users",
|
||||
"dependencies": ["users", "filterQuery"]
|
||||
},
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"compute": "computeStats",
|
||||
"valueTemplate": {
|
||||
"total": "data.users.length",
|
||||
"active": "data.users.filter(status === 'active').length",
|
||||
"inactive": "data.users.filter(status === 'inactive').length"
|
||||
},
|
||||
"dependencies": ["users"]
|
||||
}
|
||||
],
|
||||
@@ -193,7 +197,7 @@
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "filteredUsers",
|
||||
"transform": "transformFilteredUsers"
|
||||
"path": "length"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +231,7 @@
|
||||
"id": "update-filter",
|
||||
"type": "set-value",
|
||||
"target": "filterQuery",
|
||||
"compute": "updateFilterQuery"
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -239,12 +243,75 @@
|
||||
"id": "users-list",
|
||||
"type": "div",
|
||||
"props": { "className": "space-y-4" },
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "filteredUsers",
|
||||
"transform": "transformUserList"
|
||||
"loop": {
|
||||
"source": "filteredUsers",
|
||||
"itemVar": "user",
|
||||
"indexVar": "userIndex"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "user-card",
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"className": "bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "user-card-content",
|
||||
"type": "CardContent",
|
||||
"props": { "className": "pt-6" },
|
||||
"children": [
|
||||
{
|
||||
"id": "user-card-row",
|
||||
"type": "div",
|
||||
"props": { "className": "flex items-start justify-between" },
|
||||
"children": [
|
||||
{
|
||||
"id": "user-card-info",
|
||||
"type": "div",
|
||||
"props": { "className": "flex-1" },
|
||||
"children": [
|
||||
{
|
||||
"id": "user-card-name",
|
||||
"type": "div",
|
||||
"props": { "className": "font-semibold text-lg mb-1" },
|
||||
"bindings": {
|
||||
"children": { "source": "user", "path": "name" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user-card-email",
|
||||
"type": "div",
|
||||
"props": { "className": "text-sm text-muted-foreground" },
|
||||
"bindings": {
|
||||
"children": { "source": "user", "path": "email" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user-card-joined",
|
||||
"type": "div",
|
||||
"props": { "className": "text-xs text-muted-foreground mt-2" },
|
||||
"bindings": {
|
||||
"children": { "source": "user", "path": "joined" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "user-card-status",
|
||||
"type": "Badge",
|
||||
"props": { "variant": "secondary" },
|
||||
"bindings": {
|
||||
"children": { "source": "user", "path": "status" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
export const computeFilteredUsers = (data: any) => {
|
||||
const query = (data.filterQuery || '').toLowerCase()
|
||||
if (!query) return data.users || []
|
||||
return (data.users || []).filter((user: any) =>
|
||||
user.name.toLowerCase().includes(query) ||
|
||||
user.email.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
export const computeStats = (data: any) => ({
|
||||
total: data.users?.length || 0,
|
||||
active: data.users?.filter((u: any) => u.status === 'active').length || 0,
|
||||
inactive: data.users?.filter((u: any) => u.status === 'inactive').length || 0,
|
||||
})
|
||||
|
||||
export const computeTodoStats = (data: any) => ({
|
||||
total: data.todos?.length || 0,
|
||||
completed: data.todos?.filter((t: any) => t.completed).length || 0,
|
||||
remaining: data.todos?.filter((t: any) => !t.completed).length || 0,
|
||||
})
|
||||
|
||||
export const computeAddTodo = (data: any) => ({
|
||||
id: Date.now(),
|
||||
text: data.newTodo,
|
||||
completed: false,
|
||||
})
|
||||
|
||||
export const updateFilterQuery = (_: any, event: any) => event?.target?.value || ''
|
||||
|
||||
export const updateNewTodo = (data: any, event: any) => event?.target?.value || ''
|
||||
|
||||
export const checkCanAddTodo = (data: any) => data.newTodo?.trim().length > 0
|
||||
|
||||
export const transformFilteredUsers = (users: any[]) => `${users?.length || 0} users`
|
||||
|
||||
export const transformUserList = (users: any[]) => (users || []).map((user: any) => ({
|
||||
type: 'Card',
|
||||
id: `user-${user.id}`,
|
||||
props: {
|
||||
className: 'bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'CardContent',
|
||||
id: `user-content-${user.id}`,
|
||||
props: { className: 'pt-6' },
|
||||
children: [
|
||||
{
|
||||
type: 'div',
|
||||
id: `user-row-${user.id}`,
|
||||
props: { className: 'flex items-start justify-between' },
|
||||
children: [
|
||||
{
|
||||
type: 'div',
|
||||
id: `user-info-${user.id}`,
|
||||
props: { className: 'flex-1' },
|
||||
children: [
|
||||
{
|
||||
type: 'div',
|
||||
id: `user-name-${user.id}`,
|
||||
props: { className: 'font-semibold text-lg mb-1', children: user.name },
|
||||
},
|
||||
{
|
||||
type: 'div',
|
||||
id: `user-email-${user.id}`,
|
||||
props: { className: 'text-sm text-muted-foreground', children: user.email },
|
||||
},
|
||||
{
|
||||
type: 'div',
|
||||
id: `user-joined-${user.id}`,
|
||||
props: { className: 'text-xs text-muted-foreground mt-2', children: `Joined ${user.joined}` },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'Badge',
|
||||
id: `user-status-${user.id}`,
|
||||
props: {
|
||||
variant: user.status === 'active' ? 'default' : 'secondary',
|
||||
children: user.status,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -1,7 +1,4 @@
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import * as computeFunctions from './compute-functions'
|
||||
|
||||
type ComputeFunctionMap = typeof computeFunctions
|
||||
|
||||
export function hydrateSchema(jsonSchema: any): PageSchema {
|
||||
// Validate basic schema structure
|
||||
@@ -13,95 +10,5 @@ export function hydrateSchema(jsonSchema: any): PageSchema {
|
||||
console.warn('Schema missing required fields: id and name')
|
||||
}
|
||||
|
||||
const schema = { ...jsonSchema }
|
||||
|
||||
if (schema.dataSources) {
|
||||
schema.dataSources = schema.dataSources.map((ds: any) => {
|
||||
if (ds.type === 'computed' && typeof ds.compute === 'string') {
|
||||
const functionName = ds.compute as keyof ComputeFunctionMap
|
||||
const computeFunction = computeFunctions[functionName]
|
||||
if (!computeFunction) {
|
||||
console.warn(`Compute function "${functionName}" not found`)
|
||||
}
|
||||
return {
|
||||
...ds,
|
||||
compute: computeFunction || (() => null)
|
||||
}
|
||||
}
|
||||
return ds
|
||||
})
|
||||
}
|
||||
|
||||
if (schema.components) {
|
||||
schema.components = hydrateComponents(schema.components)
|
||||
}
|
||||
|
||||
return schema as PageSchema
|
||||
}
|
||||
|
||||
function hydrateComponents(components: any[]): any[] {
|
||||
return components.map(component => {
|
||||
const hydratedComponent = { ...component }
|
||||
|
||||
if (component.events) {
|
||||
hydratedComponent.events = component.events.map((event: any) => {
|
||||
const hydratedEvent = { ...event }
|
||||
|
||||
if (event.condition && typeof event.condition === 'string') {
|
||||
const functionName = event.condition as keyof ComputeFunctionMap
|
||||
const conditionFunction = computeFunctions[functionName]
|
||||
if (!conditionFunction) {
|
||||
console.warn(`Condition function "${functionName}" not found`)
|
||||
}
|
||||
hydratedEvent.condition = conditionFunction || (() => false)
|
||||
}
|
||||
|
||||
if (event.actions) {
|
||||
hydratedEvent.actions = event.actions.map((action: any) => {
|
||||
if (action.compute && typeof action.compute === 'string') {
|
||||
const functionName = action.compute as keyof ComputeFunctionMap
|
||||
const computeFunction = computeFunctions[functionName]
|
||||
if (!computeFunction) {
|
||||
console.warn(`Action compute function "${functionName}" not found`)
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
compute: computeFunction || (() => null)
|
||||
}
|
||||
}
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
return hydratedEvent
|
||||
})
|
||||
}
|
||||
|
||||
if (component.bindings) {
|
||||
const hydratedBindings: Record<string, any> = {}
|
||||
for (const [key, binding] of Object.entries(component.bindings)) {
|
||||
const b = binding as any
|
||||
if (b.transform && typeof b.transform === 'string') {
|
||||
const functionName = b.transform as keyof ComputeFunctionMap
|
||||
const transformFunction = computeFunctions[functionName]
|
||||
if (!transformFunction) {
|
||||
console.warn(`Transform function "${functionName}" not found`)
|
||||
}
|
||||
hydratedBindings[key] = {
|
||||
...b,
|
||||
transform: transformFunction || ((x: any) => x)
|
||||
}
|
||||
} else {
|
||||
hydratedBindings[key] = b
|
||||
}
|
||||
}
|
||||
hydratedComponent.bindings = hydratedBindings
|
||||
}
|
||||
|
||||
if (component.children) {
|
||||
hydratedComponent.children = hydrateComponents(component.children)
|
||||
}
|
||||
|
||||
return hydratedComponent
|
||||
})
|
||||
return jsonSchema as PageSchema
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"compute": "computeTodoStats",
|
||||
"valueTemplate": {
|
||||
"total": "data.todos.length",
|
||||
"completed": "data.todos.filter(completed === true).length",
|
||||
"remaining": "data.todos.filter(completed === false).length"
|
||||
},
|
||||
"dependencies": ["todos"]
|
||||
}
|
||||
],
|
||||
@@ -194,7 +198,7 @@
|
||||
"id": "update-input",
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"compute": "updateNewTodo"
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -212,7 +216,11 @@
|
||||
"id": "add-todo",
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"compute": "computeAddTodo"
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clear-input",
|
||||
@@ -227,7 +235,7 @@
|
||||
"variant": "success"
|
||||
}
|
||||
],
|
||||
"condition": "checkCanAddTodo"
|
||||
"condition": "data.newTodo.length > 0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ export interface DataSource {
|
||||
type: DataSourceType
|
||||
key?: string
|
||||
defaultValue?: any
|
||||
compute?: (data: Record<string, any>) => any
|
||||
expression?: string
|
||||
valueTemplate?: Record<string, any>
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
@@ -63,8 +64,6 @@ export interface Action {
|
||||
path?: string
|
||||
value?: any
|
||||
params?: Record<string, any>
|
||||
// Legacy: function-based compute
|
||||
compute?: ((data: Record<string, any>, event?: any) => any) | string
|
||||
// New: JSON-friendly expression (e.g., "event.target.value", "data.fieldName")
|
||||
expression?: string
|
||||
// New: JSON template with dynamic values
|
||||
@@ -77,20 +76,20 @@ export interface Binding {
|
||||
source: string
|
||||
sourceType?: BindingSourceType
|
||||
path?: string
|
||||
transform?: string | ((value: any) => any)
|
||||
transform?: string
|
||||
}
|
||||
|
||||
export interface EventHandler {
|
||||
event: string
|
||||
actions: Action[]
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
condition?: string
|
||||
}
|
||||
|
||||
export interface JSONEventDefinition {
|
||||
action?: string
|
||||
actions?: Action[]
|
||||
payload?: Record<string, any>
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
condition?: string
|
||||
}
|
||||
|
||||
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
|
||||
|
||||
@@ -30,7 +30,8 @@ export const DataSourceSchema = z.object({
|
||||
key: z.string().optional(),
|
||||
defaultValue: z.any().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
compute: z.string().optional(),
|
||||
expression: z.string().optional(),
|
||||
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
|
||||
export const ActionConfigSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user