mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Compare commits
1 Commits
codex/exte
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dfd891e24 |
@@ -1,62 +0,0 @@
|
||||
# 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.
|
||||
@@ -80,11 +80,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "ComponentBindingDialog",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "ComponentBindingDialogWrapper"
|
||||
}
|
||||
"wrapperFor": "ComponentBindingDialog"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
@@ -126,11 +122,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "DataSourceEditorDialog",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "DataSourceEditorDialogWrapper"
|
||||
}
|
||||
"wrapperFor": "DataSourceEditorDialog"
|
||||
},
|
||||
{
|
||||
"type": "Dialog",
|
||||
@@ -725,500 +717,6 @@
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
"type": "ArrowLeft",
|
||||
"name": "ArrowLeft",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ArrowLeft icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "ArrowLeft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ArrowRight",
|
||||
"name": "ArrowRight",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ArrowRight icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "ArrowRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Check",
|
||||
"name": "Check",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Check icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Check"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "X",
|
||||
"name": "X",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "X icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "X"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Plus",
|
||||
"name": "Plus",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Plus icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Plus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Minus",
|
||||
"name": "Minus",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Minus icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Minus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Search",
|
||||
"name": "Search",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Search icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "MagnifyingGlass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Filter",
|
||||
"name": "Filter",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Filter icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Funnel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Download",
|
||||
"name": "Download",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Download icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Download"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Upload",
|
||||
"name": "Upload",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Upload icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Upload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Edit",
|
||||
"name": "Edit",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Edit icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "PencilSimple"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Trash",
|
||||
"name": "Trash",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Trash icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Trash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Eye",
|
||||
"name": "Eye",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Eye icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Eye"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "EyeOff",
|
||||
"name": "EyeOff",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "EyeOff icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "EyeClosed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ChevronUp",
|
||||
"name": "ChevronUp",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronUp icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "CaretUp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ChevronDown",
|
||||
"name": "ChevronDown",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronDown icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "CaretDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ChevronLeft",
|
||||
"name": "ChevronLeft",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronLeft icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "CaretLeft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ChevronRight",
|
||||
"name": "ChevronRight",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronRight icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "CaretRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Settings",
|
||||
"name": "Settings",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Settings icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Gear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "User",
|
||||
"name": "User",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "User icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "User"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Bell",
|
||||
"name": "Bell",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Bell icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Bell"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Mail",
|
||||
"name": "Mail",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Mail icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Envelope"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Calendar",
|
||||
"name": "Calendar",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Calendar icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Calendar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Clock",
|
||||
"name": "Clock",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Clock icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Clock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Star",
|
||||
"name": "Star",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Star icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Star"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Heart",
|
||||
"name": "Heart",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Heart icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Heart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Share",
|
||||
"name": "Share",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Share icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "ShareNetwork"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Link",
|
||||
"name": "Link",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Link icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "LinkSimple"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Copy",
|
||||
"name": "Copy",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Copy icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Save",
|
||||
"name": "Save",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Save icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "FloppyDisk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "RefreshCw",
|
||||
"name": "RefreshCw",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "RefreshCw icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "ArrowClockwise"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "AlertCircle",
|
||||
"name": "AlertCircle",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "AlertCircle icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "WarningCircle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Info",
|
||||
"name": "Info",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Info icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "HelpCircle",
|
||||
"name": "HelpCircle",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "HelpCircle icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "Question"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Home",
|
||||
"name": "Home",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Home icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "House"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Menu",
|
||||
"name": "Menu",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Menu icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "List"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "MoreVertical",
|
||||
"name": "MoreVertical",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "MoreVertical icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "DotsThreeVertical"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "MoreHorizontal",
|
||||
"name": "MoreHorizontal",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "MoreHorizontal icon",
|
||||
"status": "supported",
|
||||
"source": "icons",
|
||||
"loadFrom": {
|
||||
"module": "icons",
|
||||
"export": "DotsThree"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Breadcrumb",
|
||||
"name": "Breadcrumb",
|
||||
@@ -1435,11 +933,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "GitHubBuildStatus",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "GitHubBuildStatusWrapper"
|
||||
}
|
||||
"wrapperFor": "GitHubBuildStatus"
|
||||
},
|
||||
{
|
||||
"type": "InfoBox",
|
||||
@@ -1601,11 +1095,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "LazyBarChart",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "LazyBarChartWrapper"
|
||||
}
|
||||
"wrapperFor": "LazyBarChart"
|
||||
},
|
||||
{
|
||||
"type": "LazyD3BarChart",
|
||||
@@ -1628,11 +1118,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "LazyD3BarChart",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "LazyD3BarChartWrapper"
|
||||
}
|
||||
"wrapperFor": "LazyD3BarChart"
|
||||
},
|
||||
{
|
||||
"type": "LazyLineChart",
|
||||
@@ -1655,11 +1141,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "LazyLineChart",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "LazyLineChartWrapper"
|
||||
}
|
||||
"wrapperFor": "LazyLineChart"
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
@@ -1718,11 +1200,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "SeedDataManager",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "SeedDataManagerWrapper"
|
||||
}
|
||||
"wrapperFor": "SeedDataManager"
|
||||
},
|
||||
{
|
||||
"type": "StatCard",
|
||||
@@ -2009,11 +1487,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "ComponentTree",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "ComponentTreeWrapper"
|
||||
}
|
||||
"wrapperFor": "ComponentTree"
|
||||
},
|
||||
{
|
||||
"type": "ComponentTreeNode",
|
||||
@@ -2256,11 +1730,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "SaveIndicator",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "SaveIndicatorWrapper"
|
||||
}
|
||||
"wrapperFor": "SaveIndicator"
|
||||
},
|
||||
{
|
||||
"type": "SchemaEditorCanvas",
|
||||
@@ -2424,11 +1894,7 @@
|
||||
"status": "json-compatible",
|
||||
"source": "wrappers",
|
||||
"jsonCompatible": true,
|
||||
"wrapperFor": "StorageSettings",
|
||||
"loadFrom": {
|
||||
"module": "wrappers",
|
||||
"export": "StorageSettingsWrapper"
|
||||
}
|
||||
"wrapperFor": "StorageSettings"
|
||||
},
|
||||
{
|
||||
"type": "Timestamp",
|
||||
@@ -2460,27 +1926,25 @@
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"total": 239,
|
||||
"supported": 226,
|
||||
"total": 222,
|
||||
"supported": 209,
|
||||
"planned": 0,
|
||||
"jsonCompatible": 50,
|
||||
"jsonCompatible": 13,
|
||||
"maybeJsonCompatible": 0,
|
||||
"byCategory": {
|
||||
"layout": 24,
|
||||
"input": 26,
|
||||
"display": 64,
|
||||
"navigation": 12,
|
||||
"feedback": 21,
|
||||
"data": 27,
|
||||
"custom": 65
|
||||
"layout": 25,
|
||||
"input": 34,
|
||||
"display": 31,
|
||||
"navigation": 15,
|
||||
"feedback": 23,
|
||||
"data": 25,
|
||||
"custom": 69
|
||||
},
|
||||
"bySource": {
|
||||
"atoms": 117,
|
||||
"molecules": 36,
|
||||
"organisms": 13,
|
||||
"ui": 25,
|
||||
"wrappers": 10,
|
||||
"icons": 38
|
||||
"molecules": 40,
|
||||
"organisms": 15,
|
||||
"ui": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "JSON Components Registry",
|
||||
"type": "object",
|
||||
"required": ["version", "description", "components"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"categories": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"category",
|
||||
"canHaveChildren",
|
||||
"description",
|
||||
"status",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"export": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"canHaveChildren": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["atoms", "molecules", "organisms", "ui", "wrappers", "icons"]
|
||||
},
|
||||
"jsonCompatible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wrapperRequired": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wrapperComponent": {
|
||||
"type": "string"
|
||||
},
|
||||
"wrapperFor": {
|
||||
"type": "string"
|
||||
},
|
||||
"loadFrom": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"module": {
|
||||
"type": "string"
|
||||
},
|
||||
"export": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replacedBy": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -7,9 +7,30 @@ 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[]>(
|
||||
dataBindingCopy.seed.dataSources as DataSource[],
|
||||
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
|
||||
)
|
||||
|
||||
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
||||
|
||||
@@ -4,11 +4,9 @@ import { useSchemaLoader } from '@/hooks/use-schema-loader'
|
||||
|
||||
interface JSONSchemaPageLoaderProps {
|
||||
schemaPath: string
|
||||
data?: Record<string, any>
|
||||
functions?: Record<string, any>
|
||||
}
|
||||
|
||||
export function JSONSchemaPageLoader({ schemaPath, data, functions }: JSONSchemaPageLoaderProps) {
|
||||
export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps) {
|
||||
const { schema, loading, error } = useSchemaLoader(schemaPath)
|
||||
|
||||
if (loading) {
|
||||
@@ -23,5 +21,5 @@ export function JSONSchemaPageLoader({ schemaPath, data, functions }: JSONSchema
|
||||
)
|
||||
}
|
||||
|
||||
return <PageRenderer schema={schema} data={data} functions={functions} />
|
||||
return <PageRenderer schema={schema} />
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ import { DataSource } from '@/types/json-ui'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
|
||||
interface ComputedSourceFieldsCopy {
|
||||
expressionLabel: string
|
||||
expressionPlaceholder: string
|
||||
expressionHelp: string
|
||||
valueTemplateLabel: string
|
||||
valueTemplatePlaceholder: string
|
||||
valueTemplateHelp: string
|
||||
computeLabel: string
|
||||
computePlaceholder: string
|
||||
computeHelp: string
|
||||
dependenciesLabel: string
|
||||
availableSourcesLabel: string
|
||||
emptyDependencies: string
|
||||
@@ -41,37 +38,22 @@ export function ComputedSourceFields({
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.expressionLabel}</Label>
|
||||
<Label>{copy.computeLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.expression || ''}
|
||||
onChange={(e) => {
|
||||
onUpdateField('expression', e.target.value)
|
||||
}}
|
||||
placeholder={copy.expressionPlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.expressionHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.valueTemplateLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.valueTemplate ? JSON.stringify(editingSource.valueTemplate, null, 2) : ''}
|
||||
value={editingSource.compute?.toString() || ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const template = JSON.parse(e.target.value)
|
||||
onUpdateField('valueTemplate', template)
|
||||
const fn = new Function('data', `return (${e.target.value})`)()
|
||||
onUpdateField('compute', fn)
|
||||
} catch (err) {
|
||||
// Invalid JSON
|
||||
// Invalid function
|
||||
}
|
||||
}}
|
||||
placeholder={copy.valueTemplatePlaceholder}
|
||||
placeholder={copy.computePlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.valueTemplateHelp}
|
||||
{copy.computeHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -247,11 +247,11 @@
|
||||
},
|
||||
{
|
||||
"id": "json-ui-schema",
|
||||
"title": "JSON UI (Schema)",
|
||||
"description": "Render JSON UI from a schema file",
|
||||
"title": "JSON UI Schema",
|
||||
"description": "Schema-driven JSON UI page",
|
||||
"icon": "Code",
|
||||
"type": "json",
|
||||
"schemaPath": "json-ui-page.json",
|
||||
"schemaPath": "json-ui-showcase-page.json",
|
||||
"layout": {
|
||||
"type": "single"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,69 @@
|
||||
import { ComponentType } from 'react'
|
||||
import { ComponentRegistry } from '@/lib/component-registry'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import { ProjectDashboard } from '@/components/ProjectDashboard'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { JSONModelDesigner } from '@/components/JSONModelDesigner'
|
||||
import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder'
|
||||
import { JSONComponentTreeManager } from '@/components/JSONComponentTreeManager'
|
||||
import { JSONWorkflowDesigner } from '@/components/JSONWorkflowDesigner'
|
||||
import { JSONLambdaDesigner } from '@/components/JSONLambdaDesigner'
|
||||
import { JSONStyleDesigner } from '@/components/JSONStyleDesigner'
|
||||
import { FileExplorer } from '@/components/FileExplorer'
|
||||
import { PlaywrightDesigner } from '@/components/PlaywrightDesigner'
|
||||
import { StorybookDesigner } from '@/components/StorybookDesigner'
|
||||
import { UnitTestDesigner } from '@/components/UnitTestDesigner'
|
||||
import { JSONFlaskDesigner } from '@/components/JSONFlaskDesigner'
|
||||
import { ProjectSettingsDesigner } from '@/components/ProjectSettingsDesigner'
|
||||
import { ErrorPanel } from '@/components/ErrorPanel'
|
||||
import { DocumentationView } from '@/components/DocumentationView'
|
||||
import { SassStylesShowcase } from '@/components/SassStylesShowcase'
|
||||
import { FeatureToggleSettings } from '@/components/FeatureToggleSettings'
|
||||
import { PWASettings } from '@/components/PWASettings'
|
||||
import { FaviconDesigner } from '@/components/FaviconDesigner'
|
||||
import { FeatureIdeaCloud } from '@/components/FeatureIdeaCloud'
|
||||
import { JSONUIShowcase } from '@/components/JSONUIShowcase'
|
||||
import { JSONConversionShowcase } from '@/components/JSONConversionShowcase'
|
||||
|
||||
export const ComponentRegistry: Record<string, ComponentType<any>> = {
|
||||
Button,
|
||||
Input,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
Badge,
|
||||
Textarea,
|
||||
|
||||
ProjectDashboard,
|
||||
CodeEditor,
|
||||
JSONModelDesigner,
|
||||
ComponentTreeBuilder,
|
||||
JSONComponentTreeManager,
|
||||
JSONWorkflowDesigner,
|
||||
JSONLambdaDesigner,
|
||||
JSONStyleDesigner,
|
||||
FileExplorer,
|
||||
PlaywrightDesigner,
|
||||
StorybookDesigner,
|
||||
UnitTestDesigner,
|
||||
JSONFlaskDesigner,
|
||||
ProjectSettingsDesigner,
|
||||
ErrorPanel,
|
||||
DocumentationView,
|
||||
SassStylesShowcase,
|
||||
FeatureToggleSettings,
|
||||
PWASettings,
|
||||
FaviconDesigner,
|
||||
FeatureIdeaCloud,
|
||||
JSONUIShowcase,
|
||||
JSONConversionShowcase,
|
||||
}
|
||||
|
||||
export function getComponent(name: string): ComponentType<any> | null {
|
||||
return ComponentRegistry[name] || null
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface PropConfig {
|
||||
/**
|
||||
* Component page prop bindings (map to stateContext).
|
||||
*/
|
||||
state?: string[]
|
||||
/**
|
||||
* Component page action bindings (map to actionContext).
|
||||
*/
|
||||
actions?: string[]
|
||||
/**
|
||||
* JSON page data bindings (map to stateContext).
|
||||
*/
|
||||
data?: string[]
|
||||
/**
|
||||
* JSON page function bindings (map to actionContext).
|
||||
*/
|
||||
functions?: string[]
|
||||
}
|
||||
|
||||
export interface ResizableConfig {
|
||||
@@ -34,10 +19,13 @@ export interface ResizableConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BasePageConfig {
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
type?: 'component' | 'json'
|
||||
component?: string
|
||||
schemaPath?: string
|
||||
enabled: boolean
|
||||
isRoot?: boolean
|
||||
toggleKey?: string
|
||||
@@ -48,22 +36,6 @@ export interface BasePageConfig {
|
||||
resizableConfig?: ResizableConfig
|
||||
}
|
||||
|
||||
export interface ComponentPageConfig extends BasePageConfig {
|
||||
type?: 'component'
|
||||
component: string
|
||||
schemaPath?: undefined
|
||||
schema?: undefined
|
||||
}
|
||||
|
||||
export interface JsonPageConfig extends BasePageConfig {
|
||||
type: 'json'
|
||||
component?: undefined
|
||||
schemaPath?: string
|
||||
schema?: PageSchema
|
||||
}
|
||||
|
||||
export type PageConfig = ComponentPageConfig | JsonPageConfig
|
||||
|
||||
export interface PagesConfig {
|
||||
pages: PageConfig[]
|
||||
}
|
||||
@@ -133,39 +105,44 @@ export function resolveProps(propConfig: PropConfig | undefined, stateContext: R
|
||||
|
||||
const resolvedProps: Record<string, any> = {}
|
||||
|
||||
const resolveEntries = (
|
||||
entries: string[] | undefined,
|
||||
context: Record<string, any>,
|
||||
label: string
|
||||
) => {
|
||||
if (!entries?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[CONFIG] 📦 Resolving', entries.length, label)
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
const [propName, contextKey] = entry.includes(':')
|
||||
? entry.split(':')
|
||||
: [entry, entry]
|
||||
|
||||
if (context[contextKey] !== undefined) {
|
||||
resolvedProps[propName] = context[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved', label, 'prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️', label, 'prop not found:', contextKey)
|
||||
try {
|
||||
if (propConfig.state) {
|
||||
console.log('[CONFIG] 📦 Resolving', propConfig.state.length, 'state props')
|
||||
for (const stateKey of propConfig.state) {
|
||||
try {
|
||||
const [propName, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
if (stateContext[contextKey] !== undefined) {
|
||||
resolvedProps[propName] = stateContext[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved state prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️ State prop not found:', contextKey)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve state prop:', stateKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (propConfig.actions) {
|
||||
console.log('[CONFIG] 🎬 Resolving', propConfig.actions.length, 'action props')
|
||||
for (const actionKey of propConfig.actions) {
|
||||
try {
|
||||
const [propName, contextKey] = actionKey.split(':')
|
||||
|
||||
if (actionContext[contextKey]) {
|
||||
resolvedProps[propName] = actionContext[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved action prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️ Action prop not found:', contextKey)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve action prop:', actionKey, err)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve', label, 'prop:', entry, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
resolveEntries(propConfig.state, stateContext, 'state')
|
||||
resolveEntries(propConfig.data, stateContext, 'data')
|
||||
resolveEntries(propConfig.actions, actionContext, 'action')
|
||||
resolveEntries(propConfig.functions, actionContext, 'function')
|
||||
} catch (err) {
|
||||
console.error('[CONFIG] ❌ Failed to resolve props:', err)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@ export const ResizableConfigSchema = z.object({
|
||||
rightPanel: ResizablePanelConfigSchema,
|
||||
})
|
||||
|
||||
const SimplePageConfigBaseSchema = z.object({
|
||||
export const SimplePageConfigSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
icon: z.string(),
|
||||
type: z.enum(['component', 'json']).optional(),
|
||||
component: z.string().optional(),
|
||||
schemaPath: z.string().optional(),
|
||||
enabled: z.boolean(),
|
||||
toggleKey: z.string().optional(),
|
||||
shortcut: z.string().optional(),
|
||||
@@ -31,21 +34,6 @@ const SimplePageConfigBaseSchema = z.object({
|
||||
resizableConfig: ResizableConfigSchema.optional(),
|
||||
})
|
||||
|
||||
const SimpleComponentPageConfigSchema = SimplePageConfigBaseSchema.extend({
|
||||
type: z.literal('component').optional(),
|
||||
component: z.string(),
|
||||
})
|
||||
|
||||
const SimpleJsonPageConfigSchema = SimplePageConfigBaseSchema.extend({
|
||||
type: z.literal('json'),
|
||||
schemaPath: z.string(),
|
||||
})
|
||||
|
||||
export const SimplePageConfigSchema = z.union([
|
||||
SimpleComponentPageConfigSchema,
|
||||
SimpleJsonPageConfigSchema,
|
||||
])
|
||||
|
||||
export const SimplePagesConfigSchema = z.object({
|
||||
pages: z.array(SimplePageConfigSchema),
|
||||
})
|
||||
@@ -79,32 +67,20 @@ export const FeatureConfigSchema = z.object({
|
||||
config: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
|
||||
const PageConfigBaseSchema = z.object({
|
||||
export const PageConfigSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
type: z.enum(['component', 'json']).optional(),
|
||||
component: z.string().optional(),
|
||||
schemaPath: z.string().optional(),
|
||||
layout: LayoutConfigSchema,
|
||||
features: z.array(FeatureConfigSchema).optional(),
|
||||
permissions: z.array(z.string()).optional(),
|
||||
shortcuts: z.array(KeyboardShortcutSchema).optional(),
|
||||
})
|
||||
|
||||
const ComponentPageConfigSchema = PageConfigBaseSchema.extend({
|
||||
type: z.literal('component').optional(),
|
||||
component: z.string(),
|
||||
})
|
||||
|
||||
const JsonPageConfigSchema = PageConfigBaseSchema.extend({
|
||||
type: z.literal('json'),
|
||||
schemaPath: z.string(),
|
||||
})
|
||||
|
||||
export const PageConfigSchema = z.union([
|
||||
ComponentPageConfigSchema,
|
||||
JsonPageConfigSchema,
|
||||
])
|
||||
|
||||
export const PageRegistrySchema = z.object({
|
||||
pages: z.array(PageConfigSchema),
|
||||
})
|
||||
|
||||
@@ -367,12 +367,12 @@
|
||||
},
|
||||
{
|
||||
"id": "json-ui-schema",
|
||||
"title": "JSON UI (Schema)",
|
||||
"title": "JSON UI Schema",
|
||||
"icon": "Code",
|
||||
"type": "json",
|
||||
"schemaPath": "json-ui-page.json",
|
||||
"schemaPath": "json-ui-showcase-page.json",
|
||||
"enabled": true,
|
||||
"order": 22.2,
|
||||
"order": 22.05,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,6 +30,8 @@ export function validatePageConfig(): ValidationError[] {
|
||||
]
|
||||
|
||||
pagesConfig.pages.forEach((page: PageConfig) => {
|
||||
const pageType = page.type ?? 'component'
|
||||
|
||||
if (!page.id) {
|
||||
errors.push({
|
||||
page: page.title || 'Unknown',
|
||||
@@ -57,9 +59,16 @@ export function validatePageConfig(): ValidationError[] {
|
||||
})
|
||||
}
|
||||
|
||||
const isJsonPage = page.type === 'json' || Boolean(page.schemaPath)
|
||||
if (page.type && !['component', 'json'].includes(page.type)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'type',
|
||||
message: `Unknown page type: ${page.type}. Expected "component" or "json".`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
if (!page.component && !isJsonPage) {
|
||||
if (pageType === 'component' && !page.component) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'component',
|
||||
@@ -68,7 +77,7 @@ export function validatePageConfig(): ValidationError[] {
|
||||
})
|
||||
}
|
||||
|
||||
if (isJsonPage && !page.schemaPath && !page.schema) {
|
||||
if (pageType === 'json' && !page.schemaPath) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'schemaPath',
|
||||
@@ -153,53 +162,44 @@ export function validatePageConfig(): ValidationError[] {
|
||||
}
|
||||
|
||||
if (page.props) {
|
||||
const validateStateKeys = (keys: string[] | undefined, field: string) => {
|
||||
if (!keys) return
|
||||
keys.forEach(stateKey => {
|
||||
const [, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
if (page.props.state) {
|
||||
page.props.state.forEach(stateKey => {
|
||||
const [, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
|
||||
if (!validStateKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field,
|
||||
field: 'props.state',
|
||||
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const validateActionKeys = (keys: string[] | undefined, field: string) => {
|
||||
if (!keys) return
|
||||
keys.forEach(actionKey => {
|
||||
const [, contextKey] = actionKey.includes(':')
|
||||
? actionKey.split(':')
|
||||
: [actionKey, actionKey]
|
||||
|
||||
|
||||
if (page.props.actions) {
|
||||
page.props.actions.forEach(actionKey => {
|
||||
const [, contextKey] = actionKey.split(':')
|
||||
|
||||
if (!contextKey) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field,
|
||||
field: 'props.actions',
|
||||
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
|
||||
severity: 'error',
|
||||
})
|
||||
} else if (!validActionKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field,
|
||||
field: 'props.actions',
|
||||
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
validateStateKeys(page.props.state, 'props.state')
|
||||
validateActionKeys(page.props.actions, 'props.actions')
|
||||
validateStateKeys(page.props.data, 'props.data')
|
||||
validateActionKeys(page.props.functions, 'props.functions')
|
||||
}
|
||||
|
||||
if (page.requiresResizable) {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"id": "displayName",
|
||||
"type": "computed",
|
||||
"dependencies": ["userProfile"],
|
||||
"expression": "data.userProfile.name"
|
||||
"computeId": "displayName"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -19,12 +19,9 @@
|
||||
"valuePlaceholder": "{\"key\": \"value\"}"
|
||||
},
|
||||
"computed": {
|
||||
"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",
|
||||
"computeLabel": "Compute Function",
|
||||
"computePlaceholder": "(data) => data.source1 + data.source2",
|
||||
"computeHelp": "Function that computes the value from other data sources",
|
||||
"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' && { expression: '', dependencies: [] }),
|
||||
...(type === 'computed' && { compute: () => null, dependencies: [] }),
|
||||
...(type === 'static' && { defaultValue: null }),
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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>>({})
|
||||
@@ -44,17 +43,14 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
||||
|
||||
computedSources.forEach(source => {
|
||||
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 }))
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [data, dataSources])
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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>>({})
|
||||
@@ -55,17 +54,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
computedSources.forEach((ds) => {
|
||||
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
|
||||
if (ds.compute && typeof ds.compute === 'function') {
|
||||
result[ds.id] = ds.compute(data)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ 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()
|
||||
@@ -47,15 +46,11 @@ export function usePage(schema: PageSchema) {
|
||||
const computed: Record<string, any> = {}
|
||||
|
||||
schema.data.forEach(source => {
|
||||
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 })
|
||||
}
|
||||
if (source.type === 'computed' && source.compute) {
|
||||
computed[source.id] = evaluateBindingExpression(source.compute, dataContext, {
|
||||
fallback: undefined,
|
||||
label: `computed data (${source.id})`,
|
||||
})
|
||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||
computed[source.id] = source.defaultValue
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
const currentData = data[action.target] || []
|
||||
|
||||
let newValue
|
||||
if (action.expression) {
|
||||
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) {
|
||||
@@ -76,7 +79,9 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
if (!targetParts) return
|
||||
|
||||
let newValue
|
||||
if (action.expression) {
|
||||
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)
|
||||
@@ -97,7 +102,9 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
const currentData = data[action.target] || []
|
||||
|
||||
let selectorValue
|
||||
if (action.expression) {
|
||||
if (action.compute) {
|
||||
selectorValue = action.compute(data, event)
|
||||
} else if (action.expression) {
|
||||
selectorValue = evaluateExpression(action.expression, evaluationContext)
|
||||
} else if (action.valueTemplate) {
|
||||
selectorValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||
@@ -122,7 +129,9 @@ export function useActionExecutor(context: JSONUIContext) {
|
||||
if (!targetParts) return
|
||||
|
||||
let newValue
|
||||
if (action.expression) {
|
||||
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)
|
||||
|
||||
@@ -69,12 +69,6 @@ export function usePWA() {
|
||||
setState(prev => ({ ...prev, isOnline: false }))
|
||||
}
|
||||
|
||||
const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
if (event.data && event.data.type === 'CACHE_CLEARED') {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
window.addEventListener('appinstalled', handleAppInstalled)
|
||||
window.addEventListener('online', handleOnline)
|
||||
@@ -102,7 +96,11 @@ export function usePWA() {
|
||||
console.error('[PWA] Service Worker registration failed:', error)
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'CACHE_CLEARED') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -110,9 +108,6 @@ export function usePWA() {
|
||||
window.removeEventListener('appinstalled', handleAppInstalled)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -87,12 +87,7 @@ export function analyzePerformance() {
|
||||
return null
|
||||
}
|
||||
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as
|
||||
| PerformanceNavigationTiming
|
||||
| undefined
|
||||
if (!navigation) {
|
||||
console.warn('[BUNDLE] ⚠️ Navigation performance entry not available')
|
||||
}
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
|
||||
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||||
|
||||
const jsResources = resources.filter(r => r.name.endsWith('.js'))
|
||||
@@ -102,11 +97,9 @@ export function analyzePerformance() {
|
||||
const totalCssSize = cssResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
|
||||
|
||||
const analysis = {
|
||||
domContentLoaded: navigation
|
||||
? navigation.domContentLoadedEventEnd - navigation.fetchStart
|
||||
: NaN,
|
||||
loadComplete: navigation ? navigation.loadEventEnd - navigation.fetchStart : NaN,
|
||||
ttfb: navigation ? navigation.responseStart - navigation.fetchStart : NaN,
|
||||
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
|
||||
loadComplete: navigation.loadEventEnd - navigation.fetchStart,
|
||||
ttfb: navigation.responseStart - navigation.fetchStart,
|
||||
resources: {
|
||||
js: {
|
||||
count: jsResources.length,
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { FlaskBlueprint } from '@/types/project'
|
||||
|
||||
import { generateFlaskBlueprint } from '../generateFlaskBlueprint'
|
||||
|
||||
const isValidIdentifier = (name: string): boolean => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
|
||||
|
||||
const extractBlueprintVariable = (code: string): { variable: string; name: string } => {
|
||||
const match = code.match(/^([A-Za-z_][A-Za-z0-9_]*)_bp = Blueprint\('([^']+)'/m)
|
||||
if (!match) {
|
||||
throw new Error('Blueprint definition not found.')
|
||||
}
|
||||
return { variable: `${match[1]}_bp`, name: match[2] }
|
||||
}
|
||||
|
||||
const extractFunctionNames = (code: string): string[] => {
|
||||
return Array.from(code.matchAll(/^def ([A-Za-z_][A-Za-z0-9_]*)\(\):/gm)).map(match => match[1])
|
||||
}
|
||||
|
||||
const extractDecoratorBlueprints = (code: string): string[] => {
|
||||
return Array.from(code.matchAll(/^@([A-Za-z_][A-Za-z0-9_]*)\.route/gm)).map(match => match[1])
|
||||
}
|
||||
|
||||
describe('generateFlaskBlueprint identifier sanitization', () => {
|
||||
it('creates valid, consistent identifiers for tricky endpoint names', () => {
|
||||
const blueprint: FlaskBlueprint = {
|
||||
id: 'bp-1',
|
||||
name: 'User Auth',
|
||||
urlPrefix: '/auth',
|
||||
description: 'Auth endpoints',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'ep-1',
|
||||
name: 'get-user',
|
||||
description: 'Fetch a user',
|
||||
method: 'GET',
|
||||
path: '/user'
|
||||
},
|
||||
{
|
||||
id: 'ep-2',
|
||||
name: '2fa',
|
||||
description: 'Two factor auth',
|
||||
method: 'POST',
|
||||
path: '/2fa'
|
||||
},
|
||||
{
|
||||
id: 'ep-3',
|
||||
name: 'user.v1',
|
||||
description: 'User v1 endpoint',
|
||||
method: 'GET',
|
||||
path: '/user/v1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const code = generateFlaskBlueprint(blueprint)
|
||||
const blueprintDefinition = extractBlueprintVariable(code)
|
||||
const functionNames = extractFunctionNames(code)
|
||||
const decoratorBlueprints = extractDecoratorBlueprints(code)
|
||||
|
||||
expect(isValidIdentifier(blueprintDefinition.name)).toBe(true)
|
||||
expect(isValidIdentifier(blueprintDefinition.variable)).toBe(true)
|
||||
expect(blueprintDefinition.variable).toBe('user_auth_bp')
|
||||
expect(blueprintDefinition.name).toBe('user_auth')
|
||||
expect(new Set(decoratorBlueprints)).toEqual(new Set([blueprintDefinition.variable]))
|
||||
|
||||
expect(functionNames).toEqual(['get_user', '_2fa', 'user_v1'])
|
||||
functionNames.forEach(name => {
|
||||
expect(isValidIdentifier(name)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FlaskConfig } from '@/types/project'
|
||||
import { generateFlaskBlueprint } from './generateFlaskBlueprint'
|
||||
import { sanitizeIdentifier } from './sanitizeIdentifier'
|
||||
|
||||
export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
const files: Record<string, string> = {}
|
||||
@@ -12,7 +11,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
appCode += `\n`
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
appCode += `from blueprints.${blueprintVarName} import ${blueprintVarName}_bp\n`
|
||||
})
|
||||
|
||||
@@ -35,7 +34,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
}
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
appCode += ` app.register_blueprint(${blueprintVarName}_bp)\n`
|
||||
})
|
||||
|
||||
@@ -51,7 +50,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
files['app.py'] = appCode
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
files[`blueprints/${blueprintVarName}.py`] = generateFlaskBlueprint(blueprint)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { FlaskBlueprint } from '@/types/project'
|
||||
import { sanitizeIdentifier } from './sanitizeIdentifier'
|
||||
|
||||
function toPythonIdentifier(value: string, fallback: string): string {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
let safe = normalized || fallback
|
||||
if (/^[0-9]/.test(safe)) {
|
||||
safe = `_${safe}`
|
||||
}
|
||||
return safe
|
||||
}
|
||||
|
||||
export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
||||
let code = `from flask import Blueprint, request, jsonify\n`
|
||||
code += `from typing import Dict, Any\n\n`
|
||||
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
code += `${blueprintVarName}_bp = Blueprint('${blueprintVarName}', __name__, url_prefix='${blueprint.urlPrefix}')\n\n`
|
||||
|
||||
blueprint.endpoints.forEach(endpoint => {
|
||||
const functionName = sanitizeIdentifier(endpoint.name, { fallback: 'endpoint' })
|
||||
const functionName = endpoint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
code += `@${blueprintVarName}_bp.route('${endpoint.path}', methods=['${endpoint.method}'])\n`
|
||||
code += `def ${functionName}():\n`
|
||||
code += ` """\n`
|
||||
@@ -45,14 +31,13 @@ export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
||||
|
||||
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
||||
endpoint.queryParams.forEach(param => {
|
||||
const paramVarName = sanitizeIdentifier(param.name, { fallback: 'param' })
|
||||
if (param.required) {
|
||||
code += ` ${paramVarName} = request.args.get('${param.name}')\n`
|
||||
code += ` if ${paramVarName} is None:\n`
|
||||
code += ` ${param.name} = request.args.get('${param.name}')\n`
|
||||
code += ` if ${param.name} is None:\n`
|
||||
code += ` return jsonify({'error': '${param.name} is required'}), 400\n\n`
|
||||
} else {
|
||||
const defaultVal = param.defaultValue || (param.type === 'string' ? "''" : param.type === 'number' ? '0' : 'None')
|
||||
code += ` ${paramVarName} = request.args.get('${param.name}', ${defaultVal})\n`
|
||||
code += ` ${param.name} = request.args.get('${param.name}', ${defaultVal})\n`
|
||||
}
|
||||
})
|
||||
code += `\n`
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
type SanitizeIdentifierOptions = {
|
||||
fallback?: string
|
||||
}
|
||||
|
||||
export function sanitizeIdentifier(value: string, options: SanitizeIdentifierOptions = {}): string {
|
||||
const fallback = options.fallback ?? 'identifier'
|
||||
const trimmed = value.trim()
|
||||
const normalized = trimmed
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.replace(/_+/g, '_')
|
||||
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (/^[0-9]/.test(normalized)) {
|
||||
return `_${normalized}`
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
@@ -1,45 +1,43 @@
|
||||
import { ComponentType } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { InputOtp } from '@/components/ui/input-otp'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Alert as ShadcnAlert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { AlertDialog } from '@/components/ui/alert-dialog'
|
||||
import { AspectRatio } from '@/components/ui/aspect-ratio'
|
||||
import { Carousel } from '@/components/ui/carousel'
|
||||
import { ChartContainer as Chart } from '@/components/ui/chart'
|
||||
import { Collapsible } from '@/components/ui/collapsible'
|
||||
import { Command } from '@/components/ui/command'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { DropdownMenu } from '@/components/ui/dropdown-menu'
|
||||
import { Menubar } from '@/components/ui/menubar'
|
||||
import { NavigationMenu } from '@/components/ui/navigation-menu'
|
||||
import { Table as ShadcnTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { ResizablePanelGroup as Resizable } from '@/components/ui/resizable'
|
||||
import { Sheet } from '@/components/ui/sheet'
|
||||
import { Sidebar } from '@/components/ui/sidebar'
|
||||
import { Toaster as Sonner } from '@/components/ui/sonner'
|
||||
import { ToggleGroup } from '@/components/ui/toggle-group'
|
||||
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
||||
import * as AtomComponents from '@/components/atoms'
|
||||
import * as MoleculeComponents from '@/components/molecules'
|
||||
import * as OrganismComponents from '@/components/organisms'
|
||||
import * as WrapperComponents from '@/lib/json-ui/wrappers'
|
||||
import {
|
||||
LazyBarChartWrapper,
|
||||
LazyD3BarChartWrapper,
|
||||
LazyLineChartWrapper,
|
||||
SaveIndicatorWrapper,
|
||||
SeedDataManagerWrapper,
|
||||
StorageSettingsWrapper,
|
||||
} from '@/lib/json-ui/wrappers'
|
||||
import jsonComponentsRegistry from '../../../json-components-registry.json'
|
||||
import * as IconComponents from '@phosphor-icons/react'
|
||||
import {
|
||||
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
|
||||
Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed,
|
||||
CaretUp, CaretDown, CaretLeft, CaretRight,
|
||||
Gear, User, Bell, Envelope, Calendar, Clock, Star,
|
||||
Heart, ShareNetwork, LinkSimple, Copy, FloppyDisk, ArrowClockwise, WarningCircle,
|
||||
Info, Question, House, List as ListIcon, DotsThreeVertical, DotsThree
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
export interface UIComponentRegistry {
|
||||
[key: string]: ComponentType<any>
|
||||
@@ -51,13 +49,6 @@ interface JsonRegistryEntry {
|
||||
export?: string
|
||||
source?: string
|
||||
status?: string
|
||||
wrapperRequired?: boolean
|
||||
wrapperComponent?: string
|
||||
wrapperFor?: string
|
||||
loadFrom?: {
|
||||
module?: string
|
||||
export?: string
|
||||
}
|
||||
deprecated?: DeprecatedComponentInfo
|
||||
}
|
||||
|
||||
@@ -71,13 +62,6 @@ export interface DeprecatedComponentInfo {
|
||||
}
|
||||
|
||||
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
||||
const componentLoaders: Record<string, Record<string, ComponentType<any>>> = {
|
||||
wrappers: WrapperComponents as Record<string, ComponentType<any>>,
|
||||
icons: IconComponents as Record<string, ComponentType<any>>,
|
||||
}
|
||||
|
||||
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
|
||||
entry.name ?? entry.type ?? entry.export
|
||||
|
||||
const buildRegistryFromNames = (
|
||||
names: string[],
|
||||
@@ -92,40 +76,11 @@ const buildRegistryFromNames = (
|
||||
}, {})
|
||||
}
|
||||
|
||||
const resolveLoadedComponent = (entry: JsonRegistryEntry): ComponentType<any> | null => {
|
||||
const moduleKey = entry.loadFrom?.module ?? entry.source
|
||||
const exportName = entry.loadFrom?.export ?? getRegistryEntryName(entry)
|
||||
if (!moduleKey || !exportName) {
|
||||
return null
|
||||
}
|
||||
const moduleComponents = componentLoaders[moduleKey]
|
||||
return moduleComponents?.[exportName] ?? null
|
||||
}
|
||||
|
||||
const buildRegistryFromEntries = (entries: JsonRegistryEntry[]): UIComponentRegistry => {
|
||||
return entries.reduce<UIComponentRegistry>((registry, entry) => {
|
||||
const registryName = getRegistryEntryName(entry)
|
||||
const component = resolveLoadedComponent(entry)
|
||||
if (registryName && component) {
|
||||
registry[registryName] = component
|
||||
}
|
||||
return registry
|
||||
}, {})
|
||||
}
|
||||
|
||||
const jsonRegistryEntries = jsonRegistry.components ?? []
|
||||
const registryEntryByType = new Map(
|
||||
jsonRegistryEntries
|
||||
.map((entry) => {
|
||||
const entryName = getRegistryEntryName(entry)
|
||||
return entryName ? [entryName, entry] : null
|
||||
})
|
||||
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
|
||||
)
|
||||
const atomComponentMap = AtomComponents as Record<string, ComponentType<any>>
|
||||
const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, DeprecatedComponentInfo>>(
|
||||
(acc, entry) => {
|
||||
const entryName = getRegistryEntryName(entry)
|
||||
const entryName = entry.export ?? entry.name ?? entry.type
|
||||
if (!entryName) {
|
||||
return acc
|
||||
}
|
||||
@@ -138,22 +93,16 @@ const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, Deprec
|
||||
)
|
||||
const atomRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'atoms')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const moleculeRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'molecules')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const organismRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'organisms')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const shadcnRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'ui')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const wrapperRegistryEntries = jsonRegistryEntries.filter((entry) => entry.source === 'wrappers')
|
||||
const iconRegistryEntries = jsonRegistryEntries.filter((entry) => entry.source === 'icons')
|
||||
|
||||
export const primitiveComponents: UIComponentRegistry = {
|
||||
div: 'div' as any,
|
||||
@@ -174,17 +123,9 @@ export const primitiveComponents: UIComponentRegistry = {
|
||||
nav: 'nav' as any,
|
||||
}
|
||||
|
||||
const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
||||
AlertDialog,
|
||||
AspectRatio,
|
||||
export const shadcnComponents: UIComponentRegistry = {
|
||||
Button,
|
||||
Carousel,
|
||||
Chart,
|
||||
Collapsible,
|
||||
Command,
|
||||
DropdownMenu,
|
||||
Input,
|
||||
InputOtp,
|
||||
Textarea,
|
||||
Label,
|
||||
Card,
|
||||
@@ -223,26 +164,13 @@ const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Menubar,
|
||||
NavigationMenu,
|
||||
Skeleton: ShadcnSkeleton,
|
||||
Pagination,
|
||||
Progress,
|
||||
Resizable,
|
||||
Sheet,
|
||||
Sidebar,
|
||||
Sonner,
|
||||
ToggleGroup,
|
||||
Avatar: ShadcnAvatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
}
|
||||
|
||||
export const shadcnComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
shadcnRegistryNames,
|
||||
shadcnComponentMap
|
||||
)
|
||||
|
||||
export const atomComponents: UIComponentRegistry = {
|
||||
...buildRegistryFromNames(
|
||||
atomRegistryNames,
|
||||
@@ -280,11 +208,55 @@ export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
OrganismComponents as Record<string, ComponentType<any>>
|
||||
)
|
||||
|
||||
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||
wrapperRegistryEntries
|
||||
)
|
||||
export const jsonWrapperComponents: UIComponentRegistry = {
|
||||
SaveIndicator: SaveIndicatorWrapper,
|
||||
LazyBarChart: LazyBarChartWrapper,
|
||||
LazyLineChart: LazyLineChartWrapper,
|
||||
LazyD3BarChart: LazyD3BarChartWrapper,
|
||||
SeedDataManager: SeedDataManagerWrapper,
|
||||
StorageSettings: StorageSettingsWrapper,
|
||||
}
|
||||
|
||||
export const iconComponents: UIComponentRegistry = buildRegistryFromEntries(iconRegistryEntries)
|
||||
export const iconComponents: UIComponentRegistry = {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
X,
|
||||
Plus,
|
||||
Minus,
|
||||
Search: MagnifyingGlass,
|
||||
Filter: Funnel,
|
||||
Download,
|
||||
Upload,
|
||||
Edit: PencilSimple,
|
||||
Trash,
|
||||
Eye,
|
||||
EyeOff: EyeClosed,
|
||||
ChevronUp: CaretUp,
|
||||
ChevronDown: CaretDown,
|
||||
ChevronLeft: CaretLeft,
|
||||
ChevronRight: CaretRight,
|
||||
Settings: Gear,
|
||||
User,
|
||||
Bell,
|
||||
Mail: Envelope,
|
||||
Calendar,
|
||||
Clock,
|
||||
Star,
|
||||
Heart,
|
||||
Share: ShareNetwork,
|
||||
Link: LinkSimple,
|
||||
Copy,
|
||||
Save: FloppyDisk,
|
||||
RefreshCw: ArrowClockwise,
|
||||
AlertCircle: WarningCircle,
|
||||
Info,
|
||||
HelpCircle: Question,
|
||||
Home: House,
|
||||
Menu: ListIcon,
|
||||
MoreVertical: DotsThreeVertical,
|
||||
MoreHorizontal: DotsThree,
|
||||
}
|
||||
|
||||
export const uiComponentRegistry: UIComponentRegistry = {
|
||||
...primitiveComponents,
|
||||
|
||||
@@ -99,7 +99,9 @@ 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
|
||||
|| evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event 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' }))
|
||||
if (conditionMet) {
|
||||
const eventPayload = typeof e === 'object' && e !== null
|
||||
? Object.assign(e as Record<string, unknown>, context)
|
||||
|
||||
@@ -34,26 +34,6 @@ 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))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Action, PageSchema } from '@/types/json-ui'
|
||||
import { useCallback } from 'react'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { useDataSources } from '@/hooks/data/use-data-sources'
|
||||
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
||||
import { useAppSelector } from '@/store'
|
||||
@@ -8,34 +8,20 @@ import { ComponentRenderer } from './component-renderer'
|
||||
interface PageRendererProps {
|
||||
schema: PageSchema
|
||||
onCustomAction?: (action: any, event?: any) => Promise<void>
|
||||
data?: Record<string, any>
|
||||
functions?: Record<string, any>
|
||||
}
|
||||
|
||||
export function PageRenderer({ schema, onCustomAction, data: externalData, functions }: PageRendererProps) {
|
||||
const { data: sourceData, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
const { data, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
const state = useAppSelector((rootState) => rootState)
|
||||
const mergedData = useMemo(() => ({ ...sourceData, ...externalData }), [externalData, sourceData])
|
||||
const executeCustomAction = useCallback(async (action: Action, event?: any) => {
|
||||
if (onCustomAction) {
|
||||
await onCustomAction(action, event)
|
||||
return
|
||||
}
|
||||
|
||||
const handler = functions?.[action.id]
|
||||
if (typeof handler === 'function') {
|
||||
await handler(action, event)
|
||||
}
|
||||
}, [functions, onCustomAction])
|
||||
|
||||
const actionContext = {
|
||||
data: mergedData,
|
||||
const context = {
|
||||
data,
|
||||
updateData,
|
||||
updatePath,
|
||||
executeAction: executeCustomAction,
|
||||
executeAction: onCustomAction || (async () => {}),
|
||||
}
|
||||
|
||||
const { executeActions } = useActionExecutor(actionContext)
|
||||
const { executeActions } = useActionExecutor(context)
|
||||
|
||||
const handleEvent = useCallback((_componentId: string, handler: { actions: any[] }, eventData: any) => {
|
||||
if (!handler?.actions?.length) return
|
||||
@@ -48,8 +34,7 @@ export function PageRenderer({ schema, onCustomAction, data: externalData, funct
|
||||
<ComponentRenderer
|
||||
key={component.id || index}
|
||||
component={component}
|
||||
data={mergedData}
|
||||
context={functions}
|
||||
data={data}
|
||||
state={state}
|
||||
onEvent={handleEvent}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ 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(),
|
||||
@@ -44,14 +45,14 @@ export const ActionSchema = z.object({
|
||||
export const EventHandlerSchema = z.object({
|
||||
event: z.string(),
|
||||
actions: z.array(ActionSchema),
|
||||
condition: z.string().optional(),
|
||||
condition: z.any().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.string().optional(),
|
||||
condition: z.any().optional(),
|
||||
})
|
||||
|
||||
export const JSONEventMapSchema = z.record(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { evaluateTransformExpression } from './expression-helpers'
|
||||
|
||||
type BindingTransform = string
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
|
||||
interface BindingSourceOptions {
|
||||
state?: Record<string, any>
|
||||
@@ -50,6 +50,10 @@ function applyTransform(value: unknown, transform?: BindingTransform) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof transform === 'function') {
|
||||
return transform(value)
|
||||
}
|
||||
|
||||
return evaluateTransformExpression(transform, value, {}, { label: 'data binding transform' })
|
||||
}
|
||||
|
||||
|
||||
@@ -78,13 +78,13 @@ export class RoutePreloadManager {
|
||||
return
|
||||
}
|
||||
|
||||
if (page.type === 'json' || page.schemaPath) {
|
||||
console.log(`[PRELOAD_MGR] 🧾 Skipping preload for JSON page: ${pageId}`)
|
||||
this.preloadedRoutes.add(pageId)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (page.type === 'json') {
|
||||
console.log(`[PRELOAD_MGR] 🧩 Skipping component preload for JSON page: ${pageId}`)
|
||||
this.preloadedRoutes.add(pageId)
|
||||
return
|
||||
}
|
||||
|
||||
const componentName = page.component as ComponentName
|
||||
console.log(`[PRELOAD_MGR] 🚀 Preloading ${pageId} → ${componentName}`)
|
||||
preloadComponentByName(componentName)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { FlaskBackendAdapter } from '../flask-backend-adapter'
|
||||
|
||||
type MockResponse = {
|
||||
ok: boolean
|
||||
status: number
|
||||
statusText: string
|
||||
text: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const createMockResponse = (status: number, body: string): MockResponse => ({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: status === 204 ? 'No Content' : 'OK',
|
||||
text: vi.fn().mockResolvedValue(body),
|
||||
})
|
||||
|
||||
describe('FlaskBackendAdapter.request', () => {
|
||||
const baseUrl = 'http://example.test'
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('resolves delete/clear when response is 204 or empty body', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(createMockResponse(204, '') as unknown as Response)
|
||||
.mockResolvedValueOnce(createMockResponse(200, '') as unknown as Response)
|
||||
|
||||
const adapter = new FlaskBackendAdapter(baseUrl)
|
||||
|
||||
await expect(adapter.delete('example-key')).resolves.toBeUndefined()
|
||||
await expect(adapter.clear()).resolves.toBeUndefined()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -24,25 +24,12 @@ export class FlaskBackendAdapter implements StorageAdapter {
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
const contentLength = response.headers.get('content-length')
|
||||
const contentType = response.headers.get('content-type')
|
||||
const hasJsonBody = contentLength !== '0' && contentType?.includes('application/json')
|
||||
|
||||
if (!response.ok) {
|
||||
const errorPayload = hasJsonBody ? await response.json().catch(() => null) : null
|
||||
const errorMessage = errorPayload?.error || response.statusText || `HTTP ${response.status}`
|
||||
throw new Error(errorMessage)
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||
throw new Error(error.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
if (response.status === 204 || !hasJsonBody) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
if (!responseText) {
|
||||
return undefined as T
|
||||
}
|
||||
return JSON.parse(responseText) as T
|
||||
return response.json()
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId)
|
||||
if (error.name === 'AbortError') {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
callOrder,
|
||||
mockFlaskGet,
|
||||
mockIndexedGet,
|
||||
mockSQLiteGet,
|
||||
mockSparkGet,
|
||||
MockFlaskBackendAdapter,
|
||||
MockIndexedDBAdapter,
|
||||
MockSQLiteAdapter,
|
||||
MockSparkKVAdapter
|
||||
} = vi.hoisted(() => {
|
||||
const callOrder: string[] = []
|
||||
const mockFlaskGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockIndexedGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockSQLiteGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockSparkGet = vi.fn<[], Promise<unknown>>()
|
||||
|
||||
class MockFlaskBackendAdapter {
|
||||
constructor() {
|
||||
callOrder.push('flask')
|
||||
}
|
||||
|
||||
get = mockFlaskGet
|
||||
}
|
||||
|
||||
class MockIndexedDBAdapter {
|
||||
constructor() {
|
||||
callOrder.push('indexeddb')
|
||||
}
|
||||
|
||||
get = mockIndexedGet
|
||||
}
|
||||
|
||||
class MockSQLiteAdapter {
|
||||
constructor() {
|
||||
callOrder.push('sqlite')
|
||||
}
|
||||
|
||||
get = mockSQLiteGet
|
||||
}
|
||||
|
||||
class MockSparkKVAdapter {
|
||||
constructor() {
|
||||
callOrder.push('sparkkv')
|
||||
}
|
||||
|
||||
get = mockSparkGet
|
||||
}
|
||||
|
||||
return {
|
||||
callOrder,
|
||||
mockFlaskGet,
|
||||
mockIndexedGet,
|
||||
mockSQLiteGet,
|
||||
mockSparkGet,
|
||||
MockFlaskBackendAdapter,
|
||||
MockIndexedDBAdapter,
|
||||
MockSQLiteAdapter,
|
||||
MockSparkKVAdapter
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./unified-storage-adapters', () => ({
|
||||
FlaskBackendAdapter: MockFlaskBackendAdapter,
|
||||
IndexedDBAdapter: MockIndexedDBAdapter,
|
||||
SQLiteAdapter: MockSQLiteAdapter,
|
||||
SparkKVAdapter: MockSparkKVAdapter
|
||||
}))
|
||||
|
||||
const createLocalStorageMock = () => {
|
||||
const store = new Map<string, string>()
|
||||
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store.set(key, value)
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
store.delete(key)
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store.clear()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('UnifiedStorage.detectAndInitialize', () => {
|
||||
let localStorageMock: ReturnType<typeof createLocalStorageMock>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
callOrder.length = 0
|
||||
mockFlaskGet.mockReset()
|
||||
mockIndexedGet.mockReset()
|
||||
mockSQLiteGet.mockReset()
|
||||
mockSparkGet.mockReset()
|
||||
|
||||
localStorageMock = createLocalStorageMock()
|
||||
vi.stubGlobal('localStorage', localStorageMock)
|
||||
vi.stubGlobal('window', { spark: undefined })
|
||||
|
||||
if (!(import.meta as { env?: Record<string, string | undefined> }).env) {
|
||||
;(import.meta as { env?: Record<string, string | undefined> }).env = {}
|
||||
}
|
||||
})
|
||||
|
||||
it('tries Flask before IndexedDB when prefer-flask is set', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-flask', 'true')
|
||||
mockFlaskGet.mockRejectedValue(new Error('flask down'))
|
||||
mockIndexedGet.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('indexedDB', {})
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
await unifiedStorage.getBackend()
|
||||
|
||||
expect(callOrder[0]).toBe('flask')
|
||||
expect(callOrder).toContain('indexeddb')
|
||||
})
|
||||
|
||||
it('falls back to IndexedDB when Flask initialization fails', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-flask', 'true')
|
||||
mockFlaskGet.mockRejectedValue(new Error('flask down'))
|
||||
mockIndexedGet.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('indexedDB', {})
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
const backend = await unifiedStorage.getBackend()
|
||||
|
||||
expect(backend).toBe('indexeddb')
|
||||
})
|
||||
|
||||
it('honors prefer-sqlite when configured', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-sqlite', 'true')
|
||||
mockSQLiteGet.mockResolvedValue(undefined)
|
||||
delete (globalThis as { indexedDB?: unknown }).indexedDB
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
const backend = await unifiedStorage.getBackend()
|
||||
|
||||
expect(backend).toBe('sqlite')
|
||||
expect(callOrder).toContain('sqlite')
|
||||
})
|
||||
})
|
||||
@@ -19,23 +19,6 @@ class UnifiedStorage {
|
||||
const flaskEnvUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true'
|
||||
|
||||
if (preferFlask || flaskEnvUrl) {
|
||||
try {
|
||||
console.log('[Storage] Flask backend explicitly configured, attempting to initialize...')
|
||||
const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl)
|
||||
await Promise.race([
|
||||
flaskAdapter.get('_health_check'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Flask connection timeout')), 2000))
|
||||
])
|
||||
this.adapter = flaskAdapter
|
||||
this.backend = 'flask'
|
||||
console.log('[Storage] ✓ Using Flask backend')
|
||||
return
|
||||
} catch (error) {
|
||||
console.warn('[Storage] Flask backend not available, falling back to IndexedDB:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof indexedDB !== 'undefined') {
|
||||
try {
|
||||
console.log('[Storage] Initializing default IndexedDB backend...')
|
||||
@@ -50,6 +33,26 @@ class UnifiedStorage {
|
||||
}
|
||||
}
|
||||
|
||||
if (preferFlask || flaskEnvUrl) {
|
||||
try {
|
||||
console.log('[Storage] Flask backend explicitly configured, attempting to initialize...')
|
||||
const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl)
|
||||
const testResponse = await Promise.race([
|
||||
flaskAdapter.get('_health_check'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Flask connection timeout')), 2000))
|
||||
])
|
||||
this.adapter = flaskAdapter
|
||||
this.backend = 'flask'
|
||||
console.log('[Storage] ✓ Using Flask backend')
|
||||
return
|
||||
} catch (error) {
|
||||
console.warn('[Storage] Flask backend not available, already using IndexedDB:', error)
|
||||
if (this.adapter && this.backend === 'indexeddb') {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preferSQLite) {
|
||||
try {
|
||||
console.log('[Storage] SQLite fallback, attempting to initialize...')
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { RouteObject, Navigate } from 'react-router-dom'
|
||||
import { LoadingFallback } from '@/components/molecules'
|
||||
import { JSONSchemaPageLoader } from '@/components/JSONSchemaPageLoader'
|
||||
import { NotFoundPage } from '@/components/NotFoundPage'
|
||||
import { JSONSchemaPageLoader } from '@/components/JSONSchemaPageLoader'
|
||||
import { getEnabledPages, resolveProps } from '@/config/page-loader'
|
||||
import { ComponentRegistry } from '@/lib/component-registry'
|
||||
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
|
||||
@@ -82,34 +81,14 @@ export function createRoutes(
|
||||
console.log('[ROUTES] 📄 Enabled pages details:', JSON.stringify(enabledPages.map(p => ({
|
||||
id: p.id,
|
||||
component: p.component,
|
||||
type: p.type,
|
||||
schemaPath: p.schemaPath,
|
||||
isRoot: p.isRoot,
|
||||
enabled: p.enabled
|
||||
})), null, 2))
|
||||
|
||||
const rootPage = enabledPages.find(p => p.isRoot)
|
||||
console.log('[ROUTES] 🏠 Root page search result:', rootPage ? `Found: ${rootPage.id} (${rootPage.component})` : 'NOT FOUND - will redirect to /dashboard')
|
||||
|
||||
// JSON page prop contract: page.props.data maps to stateContext -> data bindings,
|
||||
// page.props.functions maps to actionContext -> custom action handlers.
|
||||
// The mapping syntax matches props.state/props.actions (propName[:contextKey]).
|
||||
const renderJsonPage = (
|
||||
page: typeof enabledPages[number],
|
||||
data?: Record<string, any>,
|
||||
functions?: Record<string, any>
|
||||
) => {
|
||||
if (page.schema) {
|
||||
console.log('[ROUTES] 🧾 Rendering preloaded JSON schema for page:', page.id)
|
||||
return <PageRenderer schema={page.schema} data={data} functions={functions} />
|
||||
}
|
||||
|
||||
if (page.schemaPath) {
|
||||
console.log('[ROUTES] 🧾 Rendering JSON schema loader for page:', page.id)
|
||||
return <JSONSchemaPageLoader schemaPath={page.schemaPath} data={data} functions={functions} />
|
||||
}
|
||||
|
||||
console.error('[ROUTES] ❌ JSON page missing schemaPath:', page.id)
|
||||
return <LoadingFallback message={`Schema path missing for JSON page: ${page.id}`} />
|
||||
}
|
||||
console.log('[ROUTES] 🏠 Root page search result:', rootPage ? `Found: ${rootPage.id} (${rootPage.type ?? 'component'})` : 'NOT FOUND - will redirect to /dashboard')
|
||||
|
||||
const routes: RouteObject[] = enabledPages
|
||||
.filter(p => !p.isRoot)
|
||||
@@ -120,35 +99,18 @@ export function createRoutes(
|
||||
? resolveProps(page.props, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
if (page.type === 'json' || page.schemaPath) {
|
||||
const jsonDataConfig = page.props?.data ?? page.props?.state
|
||||
const jsonFunctionsConfig = page.props?.functions ?? page.props?.actions
|
||||
const jsonData = jsonDataConfig
|
||||
? resolveProps({ state: jsonDataConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
const jsonFunctions = jsonFunctionsConfig
|
||||
? resolveProps({ actions: jsonFunctionsConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
if (page.type === 'json' && page.schemaPath) {
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: renderJsonPage(page, jsonData, jsonFunctions)
|
||||
element: <JSONSchemaPageLoader schemaPath={page.schemaPath} />
|
||||
}
|
||||
}
|
||||
|
||||
if (page.requiresResizable && page.resizableConfig) {
|
||||
if (page.requiresResizable && page.resizableConfig && page.component) {
|
||||
console.log('[ROUTES] 🔀 Page requires resizable layout:', page.id)
|
||||
const config = page.resizableConfig
|
||||
const leftProps = resolveProps(config.leftProps, stateContext, actionContext)
|
||||
|
||||
if (!page.component) {
|
||||
console.error('[ROUTES] ❌ Resizable page missing component:', page.id)
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: <LoadingFallback message={`Component missing for page: ${page.id}`} />
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: (
|
||||
@@ -163,50 +125,31 @@ export function createRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
if (!page.component) {
|
||||
console.error('[ROUTES] ❌ Page missing component:', page.id)
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: <LoadingFallback message={`Component missing for page: ${page.id}`} />
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: <LazyComponent componentName={page.component} props={props} />
|
||||
element: page.component
|
||||
? <LazyComponent componentName={page.component} props={props} />
|
||||
: <LoadingFallback message={`Component not configured for ${page.id}`} />
|
||||
}
|
||||
})
|
||||
|
||||
if (rootPage) {
|
||||
console.log('[ROUTES] ✅ Adding root route from JSON config:', rootPage.component)
|
||||
const props = rootPage.props
|
||||
? resolveProps(rootPage.props, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
if (rootPage.type === 'json' || rootPage.schemaPath) {
|
||||
const jsonDataConfig = rootPage.props?.data ?? rootPage.props?.state
|
||||
const jsonFunctionsConfig = rootPage.props?.functions ?? rootPage.props?.actions
|
||||
const jsonData = jsonDataConfig
|
||||
? resolveProps({ state: jsonDataConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
const jsonFunctions = jsonFunctionsConfig
|
||||
? resolveProps({ actions: jsonFunctionsConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
console.log('[ROUTES] ✅ Adding root route from JSON config:', rootPage.type ?? 'component')
|
||||
if (rootPage.type === 'json' && rootPage.schemaPath) {
|
||||
routes.push({
|
||||
path: '/',
|
||||
element: renderJsonPage(rootPage, jsonData, jsonFunctions)
|
||||
})
|
||||
} else if (!rootPage.component) {
|
||||
console.error('[ROUTES] ❌ Root page missing component:', rootPage.id)
|
||||
routes.push({
|
||||
path: '/',
|
||||
element: <LoadingFallback message="Root page component missing" />
|
||||
element: <JSONSchemaPageLoader schemaPath={rootPage.schemaPath} />
|
||||
})
|
||||
} else {
|
||||
const props = rootPage.props
|
||||
? resolveProps(rootPage.props, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
routes.push({
|
||||
path: '/',
|
||||
element: <LazyComponent componentName={rootPage.component} props={props} />
|
||||
element: rootPage.component
|
||||
? <LazyComponent componentName={rootPage.component} props={props} />
|
||||
: <LoadingFallback message="Root component not configured" />
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,17 +23,13 @@
|
||||
{
|
||||
"id": "filteredUsers",
|
||||
"type": "computed",
|
||||
"expression": "data.users",
|
||||
"compute": "computeFilteredUsers",
|
||||
"dependencies": ["users", "filterQuery"]
|
||||
},
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"valueTemplate": {
|
||||
"total": "data.users.length",
|
||||
"active": "data.users.filter(status === 'active').length",
|
||||
"inactive": "data.users.filter(status === 'inactive').length"
|
||||
},
|
||||
"compute": "computeStats",
|
||||
"dependencies": ["users"]
|
||||
}
|
||||
],
|
||||
@@ -197,7 +193,7 @@
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "filteredUsers",
|
||||
"path": "length"
|
||||
"transform": "transformFilteredUsers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +227,7 @@
|
||||
"id": "update-filter",
|
||||
"type": "set-value",
|
||||
"target": "filterQuery",
|
||||
"expression": "event.target.value"
|
||||
"compute": "updateFilterQuery"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -243,75 +239,12 @@
|
||||
"id": "users-list",
|
||||
"type": "div",
|
||||
"props": { "className": "space-y-4" },
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "filteredUsers",
|
||||
"transform": "transformUserList"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
88
src/schemas/compute-functions.ts
Normal file
88
src/schemas/compute-functions.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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,4 +1,7 @@
|
||||
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
|
||||
@@ -10,5 +13,95 @@ export function hydrateSchema(jsonSchema: any): PageSchema {
|
||||
console.warn('Schema missing required fields: id and name')
|
||||
}
|
||||
|
||||
return jsonSchema as PageSchema
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,11 +23,7 @@
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"valueTemplate": {
|
||||
"total": "data.todos.length",
|
||||
"completed": "data.todos.filter(completed === true).length",
|
||||
"remaining": "data.todos.filter(completed === false).length"
|
||||
},
|
||||
"compute": "computeTodoStats",
|
||||
"dependencies": ["todos"]
|
||||
}
|
||||
],
|
||||
@@ -198,7 +194,7 @@
|
||||
"id": "update-input",
|
||||
"type": "set-value",
|
||||
"target": "newTodo",
|
||||
"expression": "event.target.value"
|
||||
"compute": "updateNewTodo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -216,11 +212,7 @@
|
||||
"id": "add-todo",
|
||||
"type": "create",
|
||||
"target": "todos",
|
||||
"valueTemplate": {
|
||||
"id": "Date.now()",
|
||||
"text": "data.newTodo",
|
||||
"completed": false
|
||||
}
|
||||
"compute": "computeAddTodo"
|
||||
},
|
||||
{
|
||||
"id": "clear-input",
|
||||
@@ -235,7 +227,7 @@
|
||||
"variant": "success"
|
||||
}
|
||||
],
|
||||
"condition": "data.newTodo.length > 0"
|
||||
"condition": "checkCanAddTodo"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ export interface DataSource {
|
||||
type: DataSourceType
|
||||
key?: string
|
||||
defaultValue?: any
|
||||
expression?: string
|
||||
valueTemplate?: Record<string, any>
|
||||
compute?: (data: Record<string, any>) => any
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
@@ -64,6 +63,8 @@ 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
|
||||
@@ -76,20 +77,20 @@ export interface Binding {
|
||||
source: string
|
||||
sourceType?: BindingSourceType
|
||||
path?: string
|
||||
transform?: string
|
||||
transform?: string | ((value: any) => any)
|
||||
}
|
||||
|
||||
export interface EventHandler {
|
||||
event: string
|
||||
actions: Action[]
|
||||
condition?: string
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
}
|
||||
|
||||
export interface JSONEventDefinition {
|
||||
action?: string
|
||||
actions?: Action[]
|
||||
payload?: Record<string, any>
|
||||
condition?: string
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
}
|
||||
|
||||
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { PageSchema } from './json-ui'
|
||||
import { PropConfig } from './prop-config'
|
||||
import { ResizableConfig } from './resizable-config'
|
||||
|
||||
export interface BasePageConfig {
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
type?: 'component' | 'json'
|
||||
component?: string
|
||||
schemaPath?: string
|
||||
schema?: string
|
||||
enabled: boolean
|
||||
isRoot?: boolean
|
||||
toggleKey?: string
|
||||
@@ -15,19 +18,3 @@ export interface BasePageConfig {
|
||||
props?: PropConfig
|
||||
resizableConfig?: ResizableConfig
|
||||
}
|
||||
|
||||
export interface ComponentPageConfig extends BasePageConfig {
|
||||
type?: 'component'
|
||||
component: string
|
||||
schemaPath?: undefined
|
||||
schema?: undefined
|
||||
}
|
||||
|
||||
export interface JsonPageConfig extends BasePageConfig {
|
||||
type: 'json'
|
||||
component?: undefined
|
||||
schemaPath?: string
|
||||
schema?: PageSchema
|
||||
}
|
||||
|
||||
export type PageConfig = ComponentPageConfig | JsonPageConfig
|
||||
|
||||
@@ -30,8 +30,7 @@ export const DataSourceSchema = z.object({
|
||||
key: z.string().optional(),
|
||||
defaultValue: z.any().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
expression: z.string().optional(),
|
||||
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||
compute: z.string().optional(),
|
||||
})
|
||||
|
||||
export const ActionConfigSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user