mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-26 06:34:54 +00:00
Compare commits
27 Commits
codex/upda
...
codex/chan
| Author | SHA1 | Date | |
|---|---|---|---|
| 418e3aa657 | |||
| cac24c0716 | |||
| 9f6b0bd871 | |||
| 1b3e0fecfe | |||
| 425797d5a2 | |||
| 9dd5a9c6d0 | |||
| adb762d0cd | |||
| e3e3dbf602 | |||
| eb8f249946 | |||
| ea21a99c8c | |||
| 90d392356d | |||
| 3d7c59557d | |||
| 818f9878aa | |||
| c618870d4b | |||
| 2641793e0f | |||
| fb971be80b | |||
| 395ab80610 | |||
| 91969e8494 | |||
| a6a44f026f | |||
| 52f1703c35 | |||
| 966b6f2aa6 | |||
| 9a8e159177 | |||
| 174f03edd2 | |||
| 42c22aa1ea | |||
| f6673e1b77 | |||
| a718aca6f5 | |||
| 7be52ffc1e |
62
JSON_EXPRESSION_SYSTEM.md
Normal file
62
JSON_EXPRESSION_SYSTEM.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# JSON Expression System
|
||||||
|
|
||||||
|
This document describes the supported JSON expression patterns used across JSON UI schemas.
|
||||||
|
Legacy compute functions have been removed in favor of expression strings and value templates.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Expressions
|
||||||
|
|
||||||
|
Expressions are string values that resolve against a data + event context:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expression": "event.target.value"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported expression patterns:
|
||||||
|
|
||||||
|
- `data` or `event`
|
||||||
|
- Dot access: `data.user.name`, `event.target.value`
|
||||||
|
- Literals: numbers, booleans, `null`, `undefined`, quoted strings
|
||||||
|
- Time: `Date.now()`
|
||||||
|
- Array filtering:
|
||||||
|
- `data.todos.filter(completed === true)`
|
||||||
|
- `data.users.filter(status === 'active').length`
|
||||||
|
|
||||||
|
### Value Templates
|
||||||
|
|
||||||
|
Value templates are JSON objects whose string values are evaluated as expressions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valueTemplate": {
|
||||||
|
"id": "Date.now()",
|
||||||
|
"text": "data.newTodo",
|
||||||
|
"completed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditions
|
||||||
|
|
||||||
|
Conditions use expression strings that are evaluated against the data context:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"condition": "data.newTodo.length > 0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported condition patterns:
|
||||||
|
|
||||||
|
- `data.field > 0`
|
||||||
|
- `data.field.length > 0`
|
||||||
|
- `data.field === 'value'`
|
||||||
|
- `data.field != null`
|
||||||
|
|
||||||
|
## Legacy Compute Functions (Removed)
|
||||||
|
|
||||||
|
Schemas should no longer reference function names in `compute`, `transform`, or string-based
|
||||||
|
condition fields. Use `expression` and `valueTemplate` instead.
|
||||||
@@ -717,6 +717,348 @@
|
|||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "atoms"
|
"source": "atoms"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "ArrowLeft",
|
||||||
|
"name": "ArrowLeft",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ArrowLeft icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ArrowRight",
|
||||||
|
"name": "ArrowRight",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ArrowRight icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Check",
|
||||||
|
"name": "Check",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Check icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "X",
|
||||||
|
"name": "X",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "X icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Plus",
|
||||||
|
"name": "Plus",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Plus icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Minus",
|
||||||
|
"name": "Minus",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Minus icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Search",
|
||||||
|
"name": "Search",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Search icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Filter",
|
||||||
|
"name": "Filter",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Filter icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Download",
|
||||||
|
"name": "Download",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Download icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Upload",
|
||||||
|
"name": "Upload",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Upload icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Edit",
|
||||||
|
"name": "Edit",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Edit icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Trash",
|
||||||
|
"name": "Trash",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Trash icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Eye",
|
||||||
|
"name": "Eye",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Eye icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "EyeOff",
|
||||||
|
"name": "EyeOff",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "EyeOff icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChevronUp",
|
||||||
|
"name": "ChevronUp",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ChevronUp icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChevronDown",
|
||||||
|
"name": "ChevronDown",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ChevronDown icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChevronLeft",
|
||||||
|
"name": "ChevronLeft",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ChevronLeft icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChevronRight",
|
||||||
|
"name": "ChevronRight",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "ChevronRight icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Settings",
|
||||||
|
"name": "Settings",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Settings icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "User",
|
||||||
|
"name": "User",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "User icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Bell",
|
||||||
|
"name": "Bell",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Bell icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Mail",
|
||||||
|
"name": "Mail",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Mail icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Calendar",
|
||||||
|
"name": "Calendar",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Calendar icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Clock",
|
||||||
|
"name": "Clock",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Clock icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Star",
|
||||||
|
"name": "Star",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Star icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Heart",
|
||||||
|
"name": "Heart",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Heart icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Share",
|
||||||
|
"name": "Share",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Share icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Link",
|
||||||
|
"name": "Link",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Link icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Copy",
|
||||||
|
"name": "Copy",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Copy icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Save",
|
||||||
|
"name": "Save",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Save icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "RefreshCw",
|
||||||
|
"name": "RefreshCw",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "RefreshCw icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AlertCircle",
|
||||||
|
"name": "AlertCircle",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "AlertCircle icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Info",
|
||||||
|
"name": "Info",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Info icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "HelpCircle",
|
||||||
|
"name": "HelpCircle",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "HelpCircle icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Home",
|
||||||
|
"name": "Home",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Home icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Menu",
|
||||||
|
"name": "Menu",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "Menu icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "MoreVertical",
|
||||||
|
"name": "MoreVertical",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "MoreVertical icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "MoreHorizontal",
|
||||||
|
"name": "MoreHorizontal",
|
||||||
|
"category": "display",
|
||||||
|
"canHaveChildren": false,
|
||||||
|
"description": "MoreHorizontal icon",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "icons"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "Breadcrumb",
|
"type": "Breadcrumb",
|
||||||
"name": "Breadcrumb",
|
"name": "Breadcrumb",
|
||||||
@@ -1926,25 +2268,27 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"total": 222,
|
"total": 239,
|
||||||
"supported": 209,
|
"supported": 226,
|
||||||
"planned": 0,
|
"planned": 0,
|
||||||
"jsonCompatible": 13,
|
"jsonCompatible": 50,
|
||||||
"maybeJsonCompatible": 0,
|
"maybeJsonCompatible": 0,
|
||||||
"byCategory": {
|
"byCategory": {
|
||||||
"layout": 25,
|
"layout": 24,
|
||||||
"input": 34,
|
"input": 26,
|
||||||
"display": 31,
|
"display": 64,
|
||||||
"navigation": 15,
|
"navigation": 12,
|
||||||
"feedback": 23,
|
"feedback": 21,
|
||||||
"data": 25,
|
"data": 27,
|
||||||
"custom": 69
|
"custom": 65
|
||||||
},
|
},
|
||||||
"bySource": {
|
"bySource": {
|
||||||
"atoms": 117,
|
"atoms": 117,
|
||||||
"molecules": 40,
|
"molecules": 36,
|
||||||
"organisms": 15,
|
"organisms": 13,
|
||||||
"ui": 50
|
"ui": 25,
|
||||||
|
"wrappers": 10,
|
||||||
|
"icons": 38
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
schemas/json-components-registry-schema.json
Normal file
102
schemas/json-components-registry-schema.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
},
|
||||||
|
"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,30 +7,9 @@ import { ComponentBindingsCard } from '@/components/data-binding-designer/Compon
|
|||||||
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
||||||
import dataBindingCopy from '@/data/data-binding-designer.json'
|
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() {
|
export function DataBindingDesigner() {
|
||||||
const [dataSources, setDataSources] = useState<DataSource[]>(
|
const [dataSources, setDataSources] = useState<DataSource[]>(
|
||||||
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
|
dataBindingCopy.seed.dataSources as DataSource[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { useSchemaLoader } from '@/hooks/use-schema-loader'
|
|||||||
|
|
||||||
interface JSONSchemaPageLoaderProps {
|
interface JSONSchemaPageLoaderProps {
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
|
data?: Record<string, any>
|
||||||
|
functions?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps) {
|
export function JSONSchemaPageLoader({ schemaPath, data, functions }: JSONSchemaPageLoaderProps) {
|
||||||
const { schema, loading, error } = useSchemaLoader(schemaPath)
|
const { schema, loading, error } = useSchemaLoader(schemaPath)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -21,5 +23,5 @@ export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageRenderer schema={schema} />
|
return <PageRenderer schema={schema} data={data} functions={functions} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { DataSource } from '@/types/json-ui'
|
|||||||
import { X } from '@phosphor-icons/react'
|
import { X } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface ComputedSourceFieldsCopy {
|
interface ComputedSourceFieldsCopy {
|
||||||
computeLabel: string
|
expressionLabel: string
|
||||||
computePlaceholder: string
|
expressionPlaceholder: string
|
||||||
computeHelp: string
|
expressionHelp: string
|
||||||
|
valueTemplateLabel: string
|
||||||
|
valueTemplatePlaceholder: string
|
||||||
|
valueTemplateHelp: string
|
||||||
dependenciesLabel: string
|
dependenciesLabel: string
|
||||||
availableSourcesLabel: string
|
availableSourcesLabel: string
|
||||||
emptyDependencies: string
|
emptyDependencies: string
|
||||||
@@ -38,22 +41,37 @@ export function ComputedSourceFields({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{copy.computeLabel}</Label>
|
<Label>{copy.expressionLabel}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={editingSource.compute?.toString() || ''}
|
value={editingSource.expression || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
try {
|
onUpdateField('expression', e.target.value)
|
||||||
const fn = new Function('data', `return (${e.target.value})`)()
|
|
||||||
onUpdateField('compute', fn)
|
|
||||||
} catch (err) {
|
|
||||||
// Invalid function
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
placeholder={copy.computePlaceholder}
|
placeholder={copy.expressionPlaceholder}
|
||||||
className="font-mono text-sm h-24"
|
className="font-mono text-sm h-24"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{copy.computeHelp}
|
{copy.expressionHelp}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{copy.valueTemplateLabel}</Label>
|
||||||
|
<Textarea
|
||||||
|
value={editingSource.valueTemplate ? JSON.stringify(editingSource.valueTemplate, null, 2) : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
try {
|
||||||
|
const template = JSON.parse(e.target.value)
|
||||||
|
onUpdateField('valueTemplate', template)
|
||||||
|
} catch (err) {
|
||||||
|
// Invalid JSON
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={copy.valueTemplatePlaceholder}
|
||||||
|
className="font-mono text-sm h-24"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{copy.valueTemplateHelp}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -247,11 +247,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "json-ui-schema",
|
"id": "json-ui-schema",
|
||||||
"title": "JSON UI Schema",
|
"title": "JSON UI (Schema)",
|
||||||
"description": "Schema-driven JSON UI page",
|
"description": "Render JSON UI from a schema file",
|
||||||
"icon": "Code",
|
"icon": "Code",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
"schemaPath": "json-ui-showcase-page.json",
|
"schemaPath": "json-ui-page.json",
|
||||||
"layout": {
|
"layout": {
|
||||||
"type": "single"
|
"type": "single"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,5 @@
|
|||||||
import { ComponentType } from 'react'
|
import { ComponentType } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { ComponentRegistry } from '@/lib/component-registry'
|
||||||
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 {
|
export function getComponent(name: string): ComponentType<any> | null {
|
||||||
return ComponentRegistry[name] || null
|
return ComponentRegistry[name] || null
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import pagesConfig from './pages.json'
|
import pagesConfig from './pages.json'
|
||||||
|
import { PageSchema } from '@/types/json-ui'
|
||||||
import { FeatureToggles } from '@/types/project'
|
import { FeatureToggles } from '@/types/project'
|
||||||
|
|
||||||
export interface PropConfig {
|
export interface PropConfig {
|
||||||
|
/**
|
||||||
|
* Component page prop bindings (map to stateContext).
|
||||||
|
*/
|
||||||
state?: string[]
|
state?: string[]
|
||||||
|
/**
|
||||||
|
* Component page action bindings (map to actionContext).
|
||||||
|
*/
|
||||||
actions?: string[]
|
actions?: string[]
|
||||||
|
/**
|
||||||
|
* JSON page data bindings (map to stateContext).
|
||||||
|
*/
|
||||||
|
data?: string[]
|
||||||
|
/**
|
||||||
|
* JSON page function bindings (map to actionContext).
|
||||||
|
*/
|
||||||
|
functions?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResizableConfig {
|
export interface ResizableConfig {
|
||||||
@@ -19,13 +34,10 @@ export interface ResizableConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageConfig {
|
export interface BasePageConfig {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
icon: string
|
icon: string
|
||||||
type?: 'component' | 'json'
|
|
||||||
component?: string
|
|
||||||
schemaPath?: string
|
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
toggleKey?: string
|
toggleKey?: string
|
||||||
@@ -36,6 +48,22 @@ export interface PageConfig {
|
|||||||
resizableConfig?: ResizableConfig
|
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 {
|
export interface PagesConfig {
|
||||||
pages: PageConfig[]
|
pages: PageConfig[]
|
||||||
}
|
}
|
||||||
@@ -105,44 +133,39 @@ export function resolveProps(propConfig: PropConfig | undefined, stateContext: R
|
|||||||
|
|
||||||
const resolvedProps: Record<string, any> = {}
|
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)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CONFIG] ❌ Failed to resolve', label, 'prop:', entry, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (propConfig.state) {
|
resolveEntries(propConfig.state, stateContext, 'state')
|
||||||
console.log('[CONFIG] 📦 Resolving', propConfig.state.length, 'state props')
|
resolveEntries(propConfig.data, stateContext, 'data')
|
||||||
for (const stateKey of propConfig.state) {
|
resolveEntries(propConfig.actions, actionContext, 'action')
|
||||||
try {
|
resolveEntries(propConfig.functions, actionContext, 'function')
|
||||||
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) {
|
} catch (err) {
|
||||||
console.error('[CONFIG] ❌ Failed to resolve props:', err)
|
console.error('[CONFIG] ❌ Failed to resolve props:', err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,10 @@ export const ResizableConfigSchema = z.object({
|
|||||||
rightPanel: ResizablePanelConfigSchema,
|
rightPanel: ResizablePanelConfigSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SimplePageConfigSchema = z.object({
|
const SimplePageConfigBaseSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
type: z.enum(['component', 'json']).optional(),
|
|
||||||
component: z.string().optional(),
|
|
||||||
schemaPath: z.string().optional(),
|
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
toggleKey: z.string().optional(),
|
toggleKey: z.string().optional(),
|
||||||
shortcut: z.string().optional(),
|
shortcut: z.string().optional(),
|
||||||
@@ -34,6 +31,21 @@ export const SimplePageConfigSchema = z.object({
|
|||||||
resizableConfig: ResizableConfigSchema.optional(),
|
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({
|
export const SimplePagesConfigSchema = z.object({
|
||||||
pages: z.array(SimplePageConfigSchema),
|
pages: z.array(SimplePageConfigSchema),
|
||||||
})
|
})
|
||||||
@@ -67,20 +79,32 @@ export const FeatureConfigSchema = z.object({
|
|||||||
config: z.record(z.string(), z.any()).optional(),
|
config: z.record(z.string(), z.any()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PageConfigSchema = z.object({
|
const PageConfigBaseSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
type: z.enum(['component', 'json']).optional(),
|
|
||||||
component: z.string().optional(),
|
|
||||||
schemaPath: z.string().optional(),
|
|
||||||
layout: LayoutConfigSchema,
|
layout: LayoutConfigSchema,
|
||||||
features: z.array(FeatureConfigSchema).optional(),
|
features: z.array(FeatureConfigSchema).optional(),
|
||||||
permissions: z.array(z.string()).optional(),
|
permissions: z.array(z.string()).optional(),
|
||||||
shortcuts: z.array(KeyboardShortcutSchema).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({
|
export const PageRegistrySchema = z.object({
|
||||||
pages: z.array(PageConfigSchema),
|
pages: z.array(PageConfigSchema),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -367,12 +367,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "json-ui-schema",
|
"id": "json-ui-schema",
|
||||||
"title": "JSON UI Schema",
|
"title": "JSON UI (Schema)",
|
||||||
"icon": "Code",
|
"icon": "Code",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
"schemaPath": "json-ui-showcase-page.json",
|
"schemaPath": "json-ui-page.json",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"order": 22.05,
|
"order": 22.2,
|
||||||
"props": {}
|
"props": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
]
|
]
|
||||||
|
|
||||||
pagesConfig.pages.forEach((page: PageConfig) => {
|
pagesConfig.pages.forEach((page: PageConfig) => {
|
||||||
const pageType = page.type ?? 'component'
|
|
||||||
|
|
||||||
if (!page.id) {
|
if (!page.id) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.title || 'Unknown',
|
page: page.title || 'Unknown',
|
||||||
@@ -59,16 +57,9 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.type && !['component', 'json'].includes(page.type)) {
|
const isJsonPage = page.type === 'json' || Boolean(page.schemaPath)
|
||||||
errors.push({
|
|
||||||
page: page.id || 'Unknown',
|
|
||||||
field: 'type',
|
|
||||||
message: `Unknown page type: ${page.type}. Expected "component" or "json".`,
|
|
||||||
severity: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageType === 'component' && !page.component) {
|
if (!page.component && !isJsonPage) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.id || 'Unknown',
|
page: page.id || 'Unknown',
|
||||||
field: 'component',
|
field: 'component',
|
||||||
@@ -77,7 +68,7 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageType === 'json' && !page.schemaPath) {
|
if (isJsonPage && !page.schemaPath && !page.schema) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.id || 'Unknown',
|
page: page.id || 'Unknown',
|
||||||
field: 'schemaPath',
|
field: 'schemaPath',
|
||||||
@@ -162,8 +153,9 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (page.props) {
|
if (page.props) {
|
||||||
if (page.props.state) {
|
const validateStateKeys = (keys: string[] | undefined, field: string) => {
|
||||||
page.props.state.forEach(stateKey => {
|
if (!keys) return
|
||||||
|
keys.forEach(stateKey => {
|
||||||
const [, contextKey] = stateKey.includes(':')
|
const [, contextKey] = stateKey.includes(':')
|
||||||
? stateKey.split(':')
|
? stateKey.split(':')
|
||||||
: [stateKey, stateKey]
|
: [stateKey, stateKey]
|
||||||
@@ -171,7 +163,7 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
if (!validStateKeys.includes(contextKey)) {
|
if (!validStateKeys.includes(contextKey)) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.id || 'Unknown',
|
page: page.id || 'Unknown',
|
||||||
field: 'props.state',
|
field,
|
||||||
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
|
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
})
|
})
|
||||||
@@ -179,27 +171,35 @@ export function validatePageConfig(): ValidationError[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.props.actions) {
|
const validateActionKeys = (keys: string[] | undefined, field: string) => {
|
||||||
page.props.actions.forEach(actionKey => {
|
if (!keys) return
|
||||||
const [, contextKey] = actionKey.split(':')
|
keys.forEach(actionKey => {
|
||||||
|
const [, contextKey] = actionKey.includes(':')
|
||||||
|
? actionKey.split(':')
|
||||||
|
: [actionKey, actionKey]
|
||||||
|
|
||||||
if (!contextKey) {
|
if (!contextKey) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.id || 'Unknown',
|
page: page.id || 'Unknown',
|
||||||
field: 'props.actions',
|
field,
|
||||||
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
|
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
})
|
})
|
||||||
} else if (!validActionKeys.includes(contextKey)) {
|
} else if (!validActionKeys.includes(contextKey)) {
|
||||||
errors.push({
|
errors.push({
|
||||||
page: page.id || 'Unknown',
|
page: page.id || 'Unknown',
|
||||||
field: 'props.actions',
|
field,
|
||||||
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
|
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
|
||||||
severity: 'error',
|
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) {
|
if (page.requiresResizable) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"id": "displayName",
|
"id": "displayName",
|
||||||
"type": "computed",
|
"type": "computed",
|
||||||
"dependencies": ["userProfile"],
|
"dependencies": ["userProfile"],
|
||||||
"computeId": "displayName"
|
"expression": "data.userProfile.name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -19,9 +19,12 @@
|
|||||||
"valuePlaceholder": "{\"key\": \"value\"}"
|
"valuePlaceholder": "{\"key\": \"value\"}"
|
||||||
},
|
},
|
||||||
"computed": {
|
"computed": {
|
||||||
"computeLabel": "Compute Function",
|
"expressionLabel": "Expression",
|
||||||
"computePlaceholder": "(data) => data.source1 + data.source2",
|
"expressionPlaceholder": "data.source1",
|
||||||
"computeHelp": "Function that computes the value from other data sources",
|
"expressionHelp": "Expression that computes the value from other data sources",
|
||||||
|
"valueTemplateLabel": "Value Template (JSON)",
|
||||||
|
"valueTemplatePlaceholder": "{\n \"total\": \"data.items.length\"\n}",
|
||||||
|
"valueTemplateHelp": "Template object with expressions for computed fields",
|
||||||
"dependenciesLabel": "Dependencies",
|
"dependenciesLabel": "Dependencies",
|
||||||
"availableSourcesLabel": "Available Sources",
|
"availableSourcesLabel": "Available Sources",
|
||||||
"emptyDependencies": "No data sources available. Create KV or static sources first."
|
"emptyDependencies": "No data sources available. Create KV or static sources first."
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
|||||||
id: `ds-${Date.now()}`,
|
id: `ds-${Date.now()}`,
|
||||||
type,
|
type,
|
||||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||||
...(type === 'computed' && { compute: () => null, dependencies: [] }),
|
...(type === 'computed' && { expression: '', dependencies: [] }),
|
||||||
...(type === 'static' && { defaultValue: null }),
|
...(type === 'static' && { defaultValue: null }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react'
|
|||||||
import { useKV } from '@/hooks/use-kv'
|
import { useKV } from '@/hooks/use-kv'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { setNestedValue } from '@/lib/json-ui/utils'
|
import { setNestedValue } from '@/lib/json-ui/utils'
|
||||||
|
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
|
||||||
export function useDataSources(dataSources: DataSource[]) {
|
export function useDataSources(dataSources: DataSource[]) {
|
||||||
const [data, setData] = useState<Record<string, any>>({})
|
const [data, setData] = useState<Record<string, any>>({})
|
||||||
@@ -43,14 +44,17 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
||||||
|
|
||||||
computedSources.forEach(source => {
|
computedSources.forEach(source => {
|
||||||
if (source.compute) {
|
const deps = source.dependencies || []
|
||||||
const deps = source.dependencies || []
|
const hasAllDeps = deps.every(dep => dep in data)
|
||||||
const hasAllDeps = deps.every(dep => dep in data)
|
|
||||||
|
|
||||||
if (hasAllDeps) {
|
if (hasAllDeps) {
|
||||||
const computedValue = source.compute(data)
|
const evaluationContext = { data }
|
||||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
const computedValue = source.expression
|
||||||
}
|
? evaluateExpression(source.expression, evaluationContext)
|
||||||
|
: source.valueTemplate
|
||||||
|
? evaluateTemplate(source.valueTemplate, evaluationContext)
|
||||||
|
: source.defaultValue
|
||||||
|
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [data, dataSources])
|
}, [data, dataSources])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
|
||||||
export function useDataSources(dataSources: DataSource[]) {
|
export function useDataSources(dataSources: DataSource[]) {
|
||||||
const [data, setData] = useState<Record<string, any>>({})
|
const [data, setData] = useState<Record<string, any>>({})
|
||||||
@@ -54,8 +55,17 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
|
||||||
computedSources.forEach((ds) => {
|
computedSources.forEach((ds) => {
|
||||||
if (ds.compute && typeof ds.compute === 'function') {
|
const evaluationContext = { data }
|
||||||
result[ds.id] = ds.compute(data)
|
if (ds.expression) {
|
||||||
|
result[ds.id] = evaluateExpression(ds.expression, evaluationContext)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ds.valueTemplate) {
|
||||||
|
result[ds.id] = evaluateTemplate(ds.valueTemplate, evaluationContext)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ds.defaultValue !== undefined) {
|
||||||
|
result[ds.id] = ds.defaultValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useWorkflows } from '../data/use-workflows'
|
|||||||
import { useLambdas } from '../data/use-lambdas'
|
import { useLambdas } from '../data/use-lambdas'
|
||||||
import { useActions } from './use-actions'
|
import { useActions } from './use-actions'
|
||||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||||
|
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
|
||||||
export function usePage(schema: PageSchema) {
|
export function usePage(schema: PageSchema) {
|
||||||
const files = useFiles()
|
const files = useFiles()
|
||||||
@@ -46,11 +47,15 @@ export function usePage(schema: PageSchema) {
|
|||||||
const computed: Record<string, any> = {}
|
const computed: Record<string, any> = {}
|
||||||
|
|
||||||
schema.data.forEach(source => {
|
schema.data.forEach(source => {
|
||||||
if (source.type === 'computed' && source.compute) {
|
if (source.type === 'computed') {
|
||||||
computed[source.id] = evaluateBindingExpression(source.compute, dataContext, {
|
if (source.expression) {
|
||||||
fallback: undefined,
|
computed[source.id] = evaluateBindingExpression(source.expression, dataContext, {
|
||||||
label: `computed data (${source.id})`,
|
fallback: undefined,
|
||||||
})
|
label: `computed data (${source.id})`,
|
||||||
|
})
|
||||||
|
} else if (source.valueTemplate) {
|
||||||
|
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: dataContext })
|
||||||
|
}
|
||||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||||
computed[source.id] = source.defaultValue
|
computed[source.id] = source.defaultValue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
|||||||
const currentData = data[action.target] || []
|
const currentData = data[action.target] || []
|
||||||
|
|
||||||
let newValue
|
let newValue
|
||||||
if (action.compute) {
|
if (action.expression) {
|
||||||
// Legacy: compute function
|
|
||||||
newValue = action.compute(data, event)
|
|
||||||
} else if (action.expression) {
|
|
||||||
// New: JSON expression
|
// New: JSON expression
|
||||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||||
} else if (action.valueTemplate) {
|
} else if (action.valueTemplate) {
|
||||||
@@ -79,9 +76,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
|||||||
if (!targetParts) return
|
if (!targetParts) return
|
||||||
|
|
||||||
let newValue
|
let newValue
|
||||||
if (action.compute) {
|
if (action.expression) {
|
||||||
newValue = action.compute(data, event)
|
|
||||||
} else if (action.expression) {
|
|
||||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||||
} else if (action.valueTemplate) {
|
} else if (action.valueTemplate) {
|
||||||
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||||
@@ -102,9 +97,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
|||||||
const currentData = data[action.target] || []
|
const currentData = data[action.target] || []
|
||||||
|
|
||||||
let selectorValue
|
let selectorValue
|
||||||
if (action.compute) {
|
if (action.expression) {
|
||||||
selectorValue = action.compute(data, event)
|
|
||||||
} else if (action.expression) {
|
|
||||||
selectorValue = evaluateExpression(action.expression, evaluationContext)
|
selectorValue = evaluateExpression(action.expression, evaluationContext)
|
||||||
} else if (action.valueTemplate) {
|
} else if (action.valueTemplate) {
|
||||||
selectorValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
selectorValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||||
@@ -129,9 +122,7 @@ export function useActionExecutor(context: JSONUIContext) {
|
|||||||
if (!targetParts) return
|
if (!targetParts) return
|
||||||
|
|
||||||
let newValue
|
let newValue
|
||||||
if (action.compute) {
|
if (action.expression) {
|
||||||
newValue = action.compute(data, event)
|
|
||||||
} else if (action.expression) {
|
|
||||||
newValue = evaluateExpression(action.expression, evaluationContext)
|
newValue = evaluateExpression(action.expression, evaluationContext)
|
||||||
} else if (action.valueTemplate) {
|
} else if (action.valueTemplate) {
|
||||||
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
newValue = evaluateTemplate(action.valueTemplate, evaluationContext)
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ export function usePWA() {
|
|||||||
setState(prev => ({ ...prev, isOnline: false }))
|
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('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||||
window.addEventListener('appinstalled', handleAppInstalled)
|
window.addEventListener('appinstalled', handleAppInstalled)
|
||||||
window.addEventListener('online', handleOnline)
|
window.addEventListener('online', handleOnline)
|
||||||
@@ -96,11 +102,7 @@ export function usePWA() {
|
|||||||
console.error('[PWA] Service Worker registration failed:', error)
|
console.error('[PWA] Service Worker registration failed:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
||||||
if (event.data && event.data.type === 'CACHE_CLEARED') {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -108,6 +110,9 @@ export function usePWA() {
|
|||||||
window.removeEventListener('appinstalled', handleAppInstalled)
|
window.removeEventListener('appinstalled', handleAppInstalled)
|
||||||
window.removeEventListener('online', handleOnline)
|
window.removeEventListener('online', handleOnline)
|
||||||
window.removeEventListener('offline', handleOffline)
|
window.removeEventListener('offline', handleOffline)
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ export function analyzePerformance() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
|
const navigation = performance.getEntriesByType('navigation')[0] as
|
||||||
|
| PerformanceNavigationTiming
|
||||||
|
| undefined
|
||||||
|
if (!navigation) {
|
||||||
|
console.warn('[BUNDLE] ⚠️ Navigation performance entry not available')
|
||||||
|
}
|
||||||
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||||||
|
|
||||||
const jsResources = resources.filter(r => r.name.endsWith('.js'))
|
const jsResources = resources.filter(r => r.name.endsWith('.js'))
|
||||||
@@ -97,9 +102,11 @@ export function analyzePerformance() {
|
|||||||
const totalCssSize = cssResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
|
const totalCssSize = cssResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
|
||||||
|
|
||||||
const analysis = {
|
const analysis = {
|
||||||
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
|
domContentLoaded: navigation
|
||||||
loadComplete: navigation.loadEventEnd - navigation.fetchStart,
|
? navigation.domContentLoadedEventEnd - navigation.fetchStart
|
||||||
ttfb: navigation.responseStart - navigation.fetchStart,
|
: NaN,
|
||||||
|
loadComplete: navigation ? navigation.loadEventEnd - navigation.fetchStart : NaN,
|
||||||
|
ttfb: navigation ? navigation.responseStart - navigation.fetchStart : NaN,
|
||||||
resources: {
|
resources: {
|
||||||
js: {
|
js: {
|
||||||
count: jsResources.length,
|
count: jsResources.length,
|
||||||
|
|||||||
73
src/lib/generators/__tests__/generateFlaskBlueprint.test.ts
Normal file
73
src/lib/generators/__tests__/generateFlaskBlueprint.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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,5 +1,6 @@
|
|||||||
import { FlaskConfig } from '@/types/project'
|
import { FlaskConfig } from '@/types/project'
|
||||||
import { generateFlaskBlueprint } from './generateFlaskBlueprint'
|
import { generateFlaskBlueprint } from './generateFlaskBlueprint'
|
||||||
|
import { sanitizeIdentifier } from './sanitizeIdentifier'
|
||||||
|
|
||||||
export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||||
const files: Record<string, string> = {}
|
const files: Record<string, string> = {}
|
||||||
@@ -11,7 +12,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
|||||||
appCode += `\n`
|
appCode += `\n`
|
||||||
|
|
||||||
config.blueprints.forEach(blueprint => {
|
config.blueprints.forEach(blueprint => {
|
||||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||||
appCode += `from blueprints.${blueprintVarName} import ${blueprintVarName}_bp\n`
|
appCode += `from blueprints.${blueprintVarName} import ${blueprintVarName}_bp\n`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.blueprints.forEach(blueprint => {
|
config.blueprints.forEach(blueprint => {
|
||||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||||
appCode += ` app.register_blueprint(${blueprintVarName}_bp)\n`
|
appCode += ` app.register_blueprint(${blueprintVarName}_bp)\n`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
|||||||
files['app.py'] = appCode
|
files['app.py'] = appCode
|
||||||
|
|
||||||
config.blueprints.forEach(blueprint => {
|
config.blueprints.forEach(blueprint => {
|
||||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||||
files[`blueprints/${blueprintVarName}.py`] = generateFlaskBlueprint(blueprint)
|
files[`blueprints/${blueprintVarName}.py`] = generateFlaskBlueprint(blueprint)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
import { FlaskBlueprint } from '@/types/project'
|
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 {
|
export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
||||||
let code = `from flask import Blueprint, request, jsonify\n`
|
let code = `from flask import Blueprint, request, jsonify\n`
|
||||||
code += `from typing import Dict, Any\n\n`
|
code += `from typing import Dict, Any\n\n`
|
||||||
|
|
||||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||||
code += `${blueprintVarName}_bp = Blueprint('${blueprintVarName}', __name__, url_prefix='${blueprint.urlPrefix}')\n\n`
|
code += `${blueprintVarName}_bp = Blueprint('${blueprintVarName}', __name__, url_prefix='${blueprint.urlPrefix}')\n\n`
|
||||||
|
|
||||||
blueprint.endpoints.forEach(endpoint => {
|
blueprint.endpoints.forEach(endpoint => {
|
||||||
const functionName = endpoint.name.toLowerCase().replace(/\s+/g, '_')
|
const functionName = sanitizeIdentifier(endpoint.name, { fallback: 'endpoint' })
|
||||||
code += `@${blueprintVarName}_bp.route('${endpoint.path}', methods=['${endpoint.method}'])\n`
|
code += `@${blueprintVarName}_bp.route('${endpoint.path}', methods=['${endpoint.method}'])\n`
|
||||||
code += `def ${functionName}():\n`
|
code += `def ${functionName}():\n`
|
||||||
code += ` """\n`
|
code += ` """\n`
|
||||||
@@ -31,13 +45,14 @@ export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
|||||||
|
|
||||||
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
||||||
endpoint.queryParams.forEach(param => {
|
endpoint.queryParams.forEach(param => {
|
||||||
|
const paramVarName = sanitizeIdentifier(param.name, { fallback: 'param' })
|
||||||
if (param.required) {
|
if (param.required) {
|
||||||
code += ` ${param.name} = request.args.get('${param.name}')\n`
|
code += ` ${paramVarName} = request.args.get('${param.name}')\n`
|
||||||
code += ` if ${param.name} is None:\n`
|
code += ` if ${paramVarName} is None:\n`
|
||||||
code += ` return jsonify({'error': '${param.name} is required'}), 400\n\n`
|
code += ` return jsonify({'error': '${param.name} is required'}), 400\n\n`
|
||||||
} else {
|
} else {
|
||||||
const defaultVal = param.defaultValue || (param.type === 'string' ? "''" : param.type === 'number' ? '0' : 'None')
|
const defaultVal = param.defaultValue || (param.type === 'string' ? "''" : param.type === 'number' ? '0' : 'None')
|
||||||
code += ` ${param.name} = request.args.get('${param.name}', ${defaultVal})\n`
|
code += ` ${paramVarName} = request.args.get('${param.name}', ${defaultVal})\n`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
code += `\n`
|
code += `\n`
|
||||||
|
|||||||
23
src/lib/generators/sanitizeIdentifier.ts
Normal file
23
src/lib/generators/sanitizeIdentifier.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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,27 +1,47 @@
|
|||||||
import { ComponentType } from 'react'
|
import { ComponentType } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { InputOtp } from '@/components/ui/input-otp'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Alert as ShadcnAlert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
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 { Switch } from '@/components/ui/switch'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
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 { Table as ShadcnTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
||||||
import { Progress } from '@/components/ui/progress'
|
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 { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
||||||
import * as AtomComponents from '@/components/atoms'
|
import * as AtomComponents from '@/components/atoms'
|
||||||
import * as MoleculeComponents from '@/components/molecules'
|
import * as MoleculeComponents from '@/components/molecules'
|
||||||
import * as OrganismComponents from '@/components/organisms'
|
import * as OrganismComponents from '@/components/organisms'
|
||||||
import {
|
import {
|
||||||
|
ComponentBindingDialogWrapper,
|
||||||
|
ComponentTreeWrapper,
|
||||||
|
DataSourceEditorDialogWrapper,
|
||||||
|
GitHubBuildStatusWrapper,
|
||||||
LazyBarChartWrapper,
|
LazyBarChartWrapper,
|
||||||
LazyD3BarChartWrapper,
|
LazyD3BarChartWrapper,
|
||||||
LazyLineChartWrapper,
|
LazyLineChartWrapper,
|
||||||
@@ -49,6 +69,9 @@ interface JsonRegistryEntry {
|
|||||||
export?: string
|
export?: string
|
||||||
source?: string
|
source?: string
|
||||||
status?: string
|
status?: string
|
||||||
|
wrapperRequired?: boolean
|
||||||
|
wrapperComponent?: string
|
||||||
|
wrapperFor?: string
|
||||||
deprecated?: DeprecatedComponentInfo
|
deprecated?: DeprecatedComponentInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +86,9 @@ export interface DeprecatedComponentInfo {
|
|||||||
|
|
||||||
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
||||||
|
|
||||||
|
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.export ?? entry.name ?? entry.type
|
||||||
|
|
||||||
const buildRegistryFromNames = (
|
const buildRegistryFromNames = (
|
||||||
names: string[],
|
names: string[],
|
||||||
components: Record<string, ComponentType<any>>
|
components: Record<string, ComponentType<any>>
|
||||||
@@ -77,10 +103,18 @@ const buildRegistryFromNames = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonRegistryEntries = jsonRegistry.components ?? []
|
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 atomComponentMap = AtomComponents as Record<string, ComponentType<any>>
|
||||||
const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, DeprecatedComponentInfo>>(
|
const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, DeprecatedComponentInfo>>(
|
||||||
(acc, entry) => {
|
(acc, entry) => {
|
||||||
const entryName = entry.export ?? entry.name ?? entry.type
|
const entryName = getRegistryEntryName(entry)
|
||||||
if (!entryName) {
|
if (!entryName) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
@@ -93,15 +127,27 @@ const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, Deprec
|
|||||||
)
|
)
|
||||||
const atomRegistryNames = jsonRegistryEntries
|
const atomRegistryNames = jsonRegistryEntries
|
||||||
.filter((entry) => entry.source === 'atoms')
|
.filter((entry) => entry.source === 'atoms')
|
||||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
.filter((name): name is string => Boolean(name))
|
.filter((name): name is string => Boolean(name))
|
||||||
const moleculeRegistryNames = jsonRegistryEntries
|
const moleculeRegistryNames = jsonRegistryEntries
|
||||||
.filter((entry) => entry.source === 'molecules')
|
.filter((entry) => entry.source === 'molecules')
|
||||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
.filter((name): name is string => Boolean(name))
|
.filter((name): name is string => Boolean(name))
|
||||||
const organismRegistryNames = jsonRegistryEntries
|
const organismRegistryNames = jsonRegistryEntries
|
||||||
.filter((entry) => entry.source === 'organisms')
|
.filter((entry) => entry.source === 'organisms')
|
||||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
|
.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 wrapperRegistryNames = jsonRegistryEntries
|
||||||
|
.filter((entry) => entry.source === 'wrappers')
|
||||||
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
|
.filter((name): name is string => Boolean(name))
|
||||||
|
const iconRegistryNames = jsonRegistryEntries
|
||||||
|
.filter((entry) => entry.source === 'icons')
|
||||||
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
.filter((name): name is string => Boolean(name))
|
.filter((name): name is string => Boolean(name))
|
||||||
|
|
||||||
export const primitiveComponents: UIComponentRegistry = {
|
export const primitiveComponents: UIComponentRegistry = {
|
||||||
@@ -123,9 +169,17 @@ export const primitiveComponents: UIComponentRegistry = {
|
|||||||
nav: 'nav' as any,
|
nav: 'nav' as any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shadcnComponents: UIComponentRegistry = {
|
const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
||||||
|
AlertDialog,
|
||||||
|
AspectRatio,
|
||||||
Button,
|
Button,
|
||||||
|
Carousel,
|
||||||
|
Chart,
|
||||||
|
Collapsible,
|
||||||
|
Command,
|
||||||
|
DropdownMenu,
|
||||||
Input,
|
Input,
|
||||||
|
InputOtp,
|
||||||
Textarea,
|
Textarea,
|
||||||
Label,
|
Label,
|
||||||
Card,
|
Card,
|
||||||
@@ -164,13 +218,26 @@ export const shadcnComponents: UIComponentRegistry = {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Menubar,
|
||||||
|
NavigationMenu,
|
||||||
Skeleton: ShadcnSkeleton,
|
Skeleton: ShadcnSkeleton,
|
||||||
|
Pagination,
|
||||||
Progress,
|
Progress,
|
||||||
|
Resizable,
|
||||||
|
Sheet,
|
||||||
|
Sidebar,
|
||||||
|
Sonner,
|
||||||
|
ToggleGroup,
|
||||||
Avatar: ShadcnAvatar,
|
Avatar: ShadcnAvatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shadcnComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||||
|
shadcnRegistryNames,
|
||||||
|
shadcnComponentMap
|
||||||
|
)
|
||||||
|
|
||||||
export const atomComponents: UIComponentRegistry = {
|
export const atomComponents: UIComponentRegistry = {
|
||||||
...buildRegistryFromNames(
|
...buildRegistryFromNames(
|
||||||
atomRegistryNames,
|
atomRegistryNames,
|
||||||
@@ -208,16 +275,25 @@ export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
|
|||||||
OrganismComponents as Record<string, ComponentType<any>>
|
OrganismComponents as Record<string, ComponentType<any>>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const jsonWrapperComponents: UIComponentRegistry = {
|
const wrapperComponentMap: Record<string, ComponentType<any>> = {
|
||||||
SaveIndicator: SaveIndicatorWrapper,
|
ComponentBindingDialogWrapper,
|
||||||
LazyBarChart: LazyBarChartWrapper,
|
ComponentTreeWrapper,
|
||||||
LazyLineChart: LazyLineChartWrapper,
|
DataSourceEditorDialogWrapper,
|
||||||
LazyD3BarChart: LazyD3BarChartWrapper,
|
GitHubBuildStatusWrapper,
|
||||||
SeedDataManager: SeedDataManagerWrapper,
|
SaveIndicatorWrapper,
|
||||||
StorageSettings: StorageSettingsWrapper,
|
LazyBarChartWrapper,
|
||||||
|
LazyLineChartWrapper,
|
||||||
|
LazyD3BarChartWrapper,
|
||||||
|
SeedDataManagerWrapper,
|
||||||
|
StorageSettingsWrapper,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const iconComponents: UIComponentRegistry = {
|
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||||
|
wrapperRegistryNames,
|
||||||
|
wrapperComponentMap
|
||||||
|
)
|
||||||
|
|
||||||
|
const iconComponentMap: Record<string, ComponentType<any>> = {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Check,
|
Check,
|
||||||
@@ -258,6 +334,11 @@ export const iconComponents: UIComponentRegistry = {
|
|||||||
MoreHorizontal: DotsThree,
|
MoreHorizontal: DotsThree,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const iconComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||||
|
iconRegistryNames,
|
||||||
|
iconComponentMap
|
||||||
|
)
|
||||||
|
|
||||||
export const uiComponentRegistry: UIComponentRegistry = {
|
export const uiComponentRegistry: UIComponentRegistry = {
|
||||||
...primitiveComponents,
|
...primitiveComponents,
|
||||||
...shadcnComponents,
|
...shadcnComponents,
|
||||||
|
|||||||
@@ -99,9 +99,7 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven
|
|||||||
resolvedEventHandlers.forEach(handler => {
|
resolvedEventHandlers.forEach(handler => {
|
||||||
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
||||||
const conditionMet = !handler.condition
|
const conditionMet = !handler.condition
|
||||||
|| (typeof handler.condition === 'function'
|
|| evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event handler condition' })
|
||||||
? handler.condition(mergedData as Record<string, any>)
|
|
||||||
: evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event handler condition' }))
|
|
||||||
if (conditionMet) {
|
if (conditionMet) {
|
||||||
const eventPayload = typeof e === 'object' && e !== null
|
const eventPayload = typeof e === 'object' && e !== null
|
||||||
? Object.assign(e as Record<string, unknown>, context)
|
? Object.assign(e as Record<string, unknown>, context)
|
||||||
|
|||||||
@@ -34,6 +34,26 @@ export function evaluateExpression(
|
|||||||
return data
|
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"
|
// Handle direct data access: "data.fieldName"
|
||||||
if (expression.startsWith('data.')) {
|
if (expression.startsWith('data.')) {
|
||||||
return getNestedValue(data, expression.substring(5))
|
return getNestedValue(data, expression.substring(5))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { PageSchema } from '@/types/json-ui'
|
import { Action, PageSchema } from '@/types/json-ui'
|
||||||
import { useDataSources } from '@/hooks/data/use-data-sources'
|
import { useDataSources } from '@/hooks/data/use-data-sources'
|
||||||
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
||||||
import { useAppSelector } from '@/store'
|
import { useAppSelector } from '@/store'
|
||||||
@@ -8,20 +8,34 @@ import { ComponentRenderer } from './component-renderer'
|
|||||||
interface PageRendererProps {
|
interface PageRendererProps {
|
||||||
schema: PageSchema
|
schema: PageSchema
|
||||||
onCustomAction?: (action: any, event?: any) => Promise<void>
|
onCustomAction?: (action: any, event?: any) => Promise<void>
|
||||||
|
data?: Record<string, any>
|
||||||
|
functions?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
export function PageRenderer({ schema, onCustomAction, data: externalData, functions }: PageRendererProps) {
|
||||||
const { data, updateData, updatePath } = useDataSources(schema.dataSources)
|
const { data: sourceData, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||||
const state = useAppSelector((rootState) => rootState)
|
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 context = {
|
const handler = functions?.[action.id]
|
||||||
data,
|
if (typeof handler === 'function') {
|
||||||
|
await handler(action, event)
|
||||||
|
}
|
||||||
|
}, [functions, onCustomAction])
|
||||||
|
|
||||||
|
const actionContext = {
|
||||||
|
data: mergedData,
|
||||||
updateData,
|
updateData,
|
||||||
updatePath,
|
updatePath,
|
||||||
executeAction: onCustomAction || (async () => {}),
|
executeAction: executeCustomAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { executeActions } = useActionExecutor(context)
|
const { executeActions } = useActionExecutor(actionContext)
|
||||||
|
|
||||||
const handleEvent = useCallback((_componentId: string, handler: { actions: any[] }, eventData: any) => {
|
const handleEvent = useCallback((_componentId: string, handler: { actions: any[] }, eventData: any) => {
|
||||||
if (!handler?.actions?.length) return
|
if (!handler?.actions?.length) return
|
||||||
@@ -34,7 +48,8 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
|||||||
<ComponentRenderer
|
<ComponentRenderer
|
||||||
key={component.id || index}
|
key={component.id || index}
|
||||||
component={component}
|
component={component}
|
||||||
data={data}
|
data={mergedData}
|
||||||
|
context={functions}
|
||||||
state={state}
|
state={state}
|
||||||
onEvent={handleEvent}
|
onEvent={handleEvent}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const ActionSchema = z.object({
|
|||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
value: z.any().optional(),
|
value: z.any().optional(),
|
||||||
params: z.record(z.string(), z.any()).optional(),
|
params: z.record(z.string(), z.any()).optional(),
|
||||||
compute: z.any().optional(),
|
|
||||||
expression: z.string().optional(),
|
expression: z.string().optional(),
|
||||||
valueTemplate: z.record(z.string(), z.any()).optional(),
|
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
@@ -45,14 +44,14 @@ export const ActionSchema = z.object({
|
|||||||
export const EventHandlerSchema = z.object({
|
export const EventHandlerSchema = z.object({
|
||||||
event: z.string(),
|
event: z.string(),
|
||||||
actions: z.array(ActionSchema),
|
actions: z.array(ActionSchema),
|
||||||
condition: z.any().optional(),
|
condition: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const JSONEventDefinitionSchema = z.object({
|
export const JSONEventDefinitionSchema = z.object({
|
||||||
action: z.string().optional(),
|
action: z.string().optional(),
|
||||||
actions: z.array(ActionSchema).optional(),
|
actions: z.array(ActionSchema).optional(),
|
||||||
payload: z.record(z.string(), z.any()).optional(),
|
payload: z.record(z.string(), z.any()).optional(),
|
||||||
condition: z.any().optional(),
|
condition: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const JSONEventMapSchema = z.record(
|
export const JSONEventMapSchema = z.record(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { evaluateTransformExpression } from './expression-helpers'
|
import { evaluateTransformExpression } from './expression-helpers'
|
||||||
|
|
||||||
type BindingTransform = string | ((data: unknown) => unknown)
|
type BindingTransform = string
|
||||||
|
|
||||||
interface BindingSourceOptions {
|
interface BindingSourceOptions {
|
||||||
state?: Record<string, any>
|
state?: Record<string, any>
|
||||||
@@ -50,10 +50,6 @@ function applyTransform(value: unknown, transform?: BindingTransform) {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof transform === 'function') {
|
|
||||||
return transform(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluateTransformExpression(transform, value, {}, { label: 'data binding transform' })
|
return evaluateTransformExpression(transform, value, {}, { label: 'data binding transform' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ export class RoutePreloadManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (page.type === 'json' || page.schemaPath) {
|
||||||
if (page.type === 'json') {
|
console.log(`[PRELOAD_MGR] 🧾 Skipping preload for JSON page: ${pageId}`)
|
||||||
console.log(`[PRELOAD_MGR] 🧩 Skipping component preload for JSON page: ${pageId}`)
|
this.preloadedRoutes.add(pageId)
|
||||||
this.preloadedRoutes.add(pageId)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
const componentName = page.component as ComponentName
|
const componentName = page.component as ComponentName
|
||||||
console.log(`[PRELOAD_MGR] 🚀 Preloading ${pageId} → ${componentName}`)
|
console.log(`[PRELOAD_MGR] 🚀 Preloading ${pageId} → ${componentName}`)
|
||||||
preloadComponentByName(componentName)
|
preloadComponentByName(componentName)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -25,11 +25,28 @@ export class FlaskBackendAdapter implements StorageAdapter {
|
|||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
let errorMessage = response.statusText
|
||||||
throw new Error(error.error || `HTTP ${response.status}`)
|
try {
|
||||||
|
const errorText = await response.text()
|
||||||
|
if (errorText) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(errorText) as { error?: string }
|
||||||
|
errorMessage = parsed.error || errorText
|
||||||
|
} catch {
|
||||||
|
errorMessage = errorText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore error parsing failures
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage || `HTTP ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json()
|
const responseText = await response.text()
|
||||||
|
if (!responseText) {
|
||||||
|
return undefined as T
|
||||||
|
}
|
||||||
|
return JSON.parse(responseText) as T
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
|
|||||||
145
src/lib/unified-storage.test.ts
Normal file
145
src/lib/unified-storage.test.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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,6 +19,23 @@ class UnifiedStorage {
|
|||||||
const flaskEnvUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
const flaskEnvUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||||
const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true'
|
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') {
|
if (typeof indexedDB !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
console.log('[Storage] Initializing default IndexedDB backend...')
|
console.log('[Storage] Initializing default IndexedDB backend...')
|
||||||
@@ -33,26 +50,6 @@ 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) {
|
if (preferSQLite) {
|
||||||
try {
|
try {
|
||||||
console.log('[Storage] SQLite fallback, attempting to initialize...')
|
console.log('[Storage] SQLite fallback, attempting to initialize...')
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { lazy, Suspense } from 'react'
|
import { lazy, Suspense } from 'react'
|
||||||
import { RouteObject, Navigate } from 'react-router-dom'
|
import { RouteObject, Navigate } from 'react-router-dom'
|
||||||
import { LoadingFallback } from '@/components/molecules'
|
import { LoadingFallback } from '@/components/molecules'
|
||||||
import { NotFoundPage } from '@/components/NotFoundPage'
|
|
||||||
import { JSONSchemaPageLoader } from '@/components/JSONSchemaPageLoader'
|
import { JSONSchemaPageLoader } from '@/components/JSONSchemaPageLoader'
|
||||||
|
import { NotFoundPage } from '@/components/NotFoundPage'
|
||||||
import { getEnabledPages, resolveProps } from '@/config/page-loader'
|
import { getEnabledPages, resolveProps } from '@/config/page-loader'
|
||||||
import { ComponentRegistry } from '@/lib/component-registry'
|
import { ComponentRegistry } from '@/lib/component-registry'
|
||||||
|
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
||||||
import { FeatureToggles } from '@/types/project'
|
import { FeatureToggles } from '@/types/project'
|
||||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||||
|
|
||||||
@@ -81,14 +82,34 @@ export function createRoutes(
|
|||||||
console.log('[ROUTES] 📄 Enabled pages details:', JSON.stringify(enabledPages.map(p => ({
|
console.log('[ROUTES] 📄 Enabled pages details:', JSON.stringify(enabledPages.map(p => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
component: p.component,
|
component: p.component,
|
||||||
type: p.type,
|
|
||||||
schemaPath: p.schemaPath,
|
|
||||||
isRoot: p.isRoot,
|
isRoot: p.isRoot,
|
||||||
enabled: p.enabled
|
enabled: p.enabled
|
||||||
})), null, 2))
|
})), null, 2))
|
||||||
|
|
||||||
const rootPage = enabledPages.find(p => p.isRoot)
|
const rootPage = enabledPages.find(p => p.isRoot)
|
||||||
console.log('[ROUTES] 🏠 Root page search result:', rootPage ? `Found: ${rootPage.id} (${rootPage.type ?? 'component'})` : 'NOT FOUND - will redirect to /dashboard')
|
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}`} />
|
||||||
|
}
|
||||||
|
|
||||||
const routes: RouteObject[] = enabledPages
|
const routes: RouteObject[] = enabledPages
|
||||||
.filter(p => !p.isRoot)
|
.filter(p => !p.isRoot)
|
||||||
@@ -99,18 +120,35 @@ export function createRoutes(
|
|||||||
? resolveProps(page.props, stateContext, actionContext)
|
? resolveProps(page.props, stateContext, actionContext)
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
if (page.type === 'json' && page.schemaPath) {
|
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)
|
||||||
|
: {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: `/${page.id}`,
|
path: `/${page.id}`,
|
||||||
element: <JSONSchemaPageLoader schemaPath={page.schemaPath} />
|
element: renderJsonPage(page, jsonData, jsonFunctions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.requiresResizable && page.resizableConfig && page.component) {
|
if (page.requiresResizable && page.resizableConfig) {
|
||||||
console.log('[ROUTES] 🔀 Page requires resizable layout:', page.id)
|
console.log('[ROUTES] 🔀 Page requires resizable layout:', page.id)
|
||||||
const config = page.resizableConfig
|
const config = page.resizableConfig
|
||||||
const leftProps = resolveProps(config.leftProps, stateContext, actionContext)
|
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 {
|
return {
|
||||||
path: `/${page.id}`,
|
path: `/${page.id}`,
|
||||||
element: (
|
element: (
|
||||||
@@ -125,31 +163,50 @@ 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 {
|
return {
|
||||||
path: `/${page.id}`,
|
path: `/${page.id}`,
|
||||||
element: page.component
|
element: <LazyComponent componentName={page.component} props={props} />
|
||||||
? <LazyComponent componentName={page.component} props={props} />
|
|
||||||
: <LoadingFallback message={`Component not configured for ${page.id}`} />
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (rootPage) {
|
if (rootPage) {
|
||||||
console.log('[ROUTES] ✅ Adding root route from JSON config:', rootPage.type ?? 'component')
|
console.log('[ROUTES] ✅ Adding root route from JSON config:', rootPage.component)
|
||||||
if (rootPage.type === 'json' && rootPage.schemaPath) {
|
const props = rootPage.props
|
||||||
routes.push({
|
? resolveProps(rootPage.props, stateContext, actionContext)
|
||||||
path: '/',
|
: {}
|
||||||
element: <JSONSchemaPageLoader schemaPath={rootPage.schemaPath} />
|
|
||||||
})
|
if (rootPage.type === 'json' || rootPage.schemaPath) {
|
||||||
} else {
|
const jsonDataConfig = rootPage.props?.data ?? rootPage.props?.state
|
||||||
const props = rootPage.props
|
const jsonFunctionsConfig = rootPage.props?.functions ?? rootPage.props?.actions
|
||||||
? resolveProps(rootPage.props, stateContext, actionContext)
|
const jsonData = jsonDataConfig
|
||||||
|
? resolveProps({ state: jsonDataConfig }, stateContext, actionContext)
|
||||||
|
: {}
|
||||||
|
const jsonFunctions = jsonFunctionsConfig
|
||||||
|
? resolveProps({ actions: jsonFunctionsConfig }, stateContext, actionContext)
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
path: '/',
|
path: '/',
|
||||||
element: rootPage.component
|
element: renderJsonPage(rootPage, jsonData, jsonFunctions)
|
||||||
? <LazyComponent componentName={rootPage.component} props={props} />
|
})
|
||||||
: <LoadingFallback message="Root component not configured" />
|
} else if (!rootPage.component) {
|
||||||
|
console.error('[ROUTES] ❌ Root page missing component:', rootPage.id)
|
||||||
|
routes.push({
|
||||||
|
path: '/',
|
||||||
|
element: <LoadingFallback message="Root page component missing" />
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
routes.push({
|
||||||
|
path: '/',
|
||||||
|
element: <LazyComponent componentName={rootPage.component} props={props} />
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,13 +23,17 @@
|
|||||||
{
|
{
|
||||||
"id": "filteredUsers",
|
"id": "filteredUsers",
|
||||||
"type": "computed",
|
"type": "computed",
|
||||||
"compute": "computeFilteredUsers",
|
"expression": "data.users",
|
||||||
"dependencies": ["users", "filterQuery"]
|
"dependencies": ["users", "filterQuery"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "computed",
|
||||||
"compute": "computeStats",
|
"valueTemplate": {
|
||||||
|
"total": "data.users.length",
|
||||||
|
"active": "data.users.filter(status === 'active').length",
|
||||||
|
"inactive": "data.users.filter(status === 'inactive').length"
|
||||||
|
},
|
||||||
"dependencies": ["users"]
|
"dependencies": ["users"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193,7 +197,7 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"children": {
|
"children": {
|
||||||
"source": "filteredUsers",
|
"source": "filteredUsers",
|
||||||
"transform": "transformFilteredUsers"
|
"path": "length"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +231,7 @@
|
|||||||
"id": "update-filter",
|
"id": "update-filter",
|
||||||
"type": "set-value",
|
"type": "set-value",
|
||||||
"target": "filterQuery",
|
"target": "filterQuery",
|
||||||
"compute": "updateFilterQuery"
|
"expression": "event.target.value"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -239,12 +243,75 @@
|
|||||||
"id": "users-list",
|
"id": "users-list",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": { "className": "space-y-4" },
|
"props": { "className": "space-y-4" },
|
||||||
"bindings": {
|
"loop": {
|
||||||
"children": {
|
"source": "filteredUsers",
|
||||||
"source": "filteredUsers",
|
"itemVar": "user",
|
||||||
"transform": "transformUserList"
|
"indexVar": "userIndex"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "user-card",
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"className": "bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "user-card-content",
|
||||||
|
"type": "CardContent",
|
||||||
|
"props": { "className": "pt-6" },
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "user-card-row",
|
||||||
|
"type": "div",
|
||||||
|
"props": { "className": "flex items-start justify-between" },
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "user-card-info",
|
||||||
|
"type": "div",
|
||||||
|
"props": { "className": "flex-1" },
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "user-card-name",
|
||||||
|
"type": "div",
|
||||||
|
"props": { "className": "font-semibold text-lg mb-1" },
|
||||||
|
"bindings": {
|
||||||
|
"children": { "source": "user", "path": "name" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-card-email",
|
||||||
|
"type": "div",
|
||||||
|
"props": { "className": "text-sm text-muted-foreground" },
|
||||||
|
"bindings": {
|
||||||
|
"children": { "source": "user", "path": "email" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-card-joined",
|
||||||
|
"type": "div",
|
||||||
|
"props": { "className": "text-xs text-muted-foreground mt-2" },
|
||||||
|
"bindings": {
|
||||||
|
"children": { "source": "user", "path": "joined" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "user-card-status",
|
||||||
|
"type": "Badge",
|
||||||
|
"props": { "variant": "secondary" },
|
||||||
|
"bindings": {
|
||||||
|
"children": { "source": "user", "path": "status" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
export const computeFilteredUsers = (data: any) => {
|
|
||||||
const query = (data.filterQuery || '').toLowerCase()
|
|
||||||
if (!query) return data.users || []
|
|
||||||
return (data.users || []).filter((user: any) =>
|
|
||||||
user.name.toLowerCase().includes(query) ||
|
|
||||||
user.email.toLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const computeStats = (data: any) => ({
|
|
||||||
total: data.users?.length || 0,
|
|
||||||
active: data.users?.filter((u: any) => u.status === 'active').length || 0,
|
|
||||||
inactive: data.users?.filter((u: any) => u.status === 'inactive').length || 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const computeTodoStats = (data: any) => ({
|
|
||||||
total: data.todos?.length || 0,
|
|
||||||
completed: data.todos?.filter((t: any) => t.completed).length || 0,
|
|
||||||
remaining: data.todos?.filter((t: any) => !t.completed).length || 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const computeAddTodo = (data: any) => ({
|
|
||||||
id: Date.now(),
|
|
||||||
text: data.newTodo,
|
|
||||||
completed: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const updateFilterQuery = (_: any, event: any) => event?.target?.value || ''
|
|
||||||
|
|
||||||
export const updateNewTodo = (data: any, event: any) => event?.target?.value || ''
|
|
||||||
|
|
||||||
export const checkCanAddTodo = (data: any) => data.newTodo?.trim().length > 0
|
|
||||||
|
|
||||||
export const transformFilteredUsers = (users: any[]) => `${users?.length || 0} users`
|
|
||||||
|
|
||||||
export const transformUserList = (users: any[]) => (users || []).map((user: any) => ({
|
|
||||||
type: 'Card',
|
|
||||||
id: `user-${user.id}`,
|
|
||||||
props: {
|
|
||||||
className: 'bg-background/50 hover:bg-background/80 transition-colors border-l-4 border-l-primary',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'CardContent',
|
|
||||||
id: `user-content-${user.id}`,
|
|
||||||
props: { className: 'pt-6' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'div',
|
|
||||||
id: `user-row-${user.id}`,
|
|
||||||
props: { className: 'flex items-start justify-between' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'div',
|
|
||||||
id: `user-info-${user.id}`,
|
|
||||||
props: { className: 'flex-1' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'div',
|
|
||||||
id: `user-name-${user.id}`,
|
|
||||||
props: { className: 'font-semibold text-lg mb-1', children: user.name },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'div',
|
|
||||||
id: `user-email-${user.id}`,
|
|
||||||
props: { className: 'text-sm text-muted-foreground', children: user.email },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'div',
|
|
||||||
id: `user-joined-${user.id}`,
|
|
||||||
props: { className: 'text-xs text-muted-foreground mt-2', children: `Joined ${user.joined}` },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Badge',
|
|
||||||
id: `user-status-${user.id}`,
|
|
||||||
props: {
|
|
||||||
variant: user.status === 'active' ? 'default' : 'secondary',
|
|
||||||
children: user.status,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import { PageSchema } from '@/types/json-ui'
|
import { PageSchema } from '@/types/json-ui'
|
||||||
import * as computeFunctions from './compute-functions'
|
|
||||||
|
|
||||||
type ComputeFunctionMap = typeof computeFunctions
|
|
||||||
|
|
||||||
export function hydrateSchema(jsonSchema: any): PageSchema {
|
export function hydrateSchema(jsonSchema: any): PageSchema {
|
||||||
// Validate basic schema structure
|
// Validate basic schema structure
|
||||||
@@ -13,95 +10,5 @@ export function hydrateSchema(jsonSchema: any): PageSchema {
|
|||||||
console.warn('Schema missing required fields: id and name')
|
console.warn('Schema missing required fields: id and name')
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = { ...jsonSchema }
|
return jsonSchema as PageSchema
|
||||||
|
|
||||||
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,7 +23,11 @@
|
|||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "computed",
|
||||||
"compute": "computeTodoStats",
|
"valueTemplate": {
|
||||||
|
"total": "data.todos.length",
|
||||||
|
"completed": "data.todos.filter(completed === true).length",
|
||||||
|
"remaining": "data.todos.filter(completed === false).length"
|
||||||
|
},
|
||||||
"dependencies": ["todos"]
|
"dependencies": ["todos"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -194,7 +198,7 @@
|
|||||||
"id": "update-input",
|
"id": "update-input",
|
||||||
"type": "set-value",
|
"type": "set-value",
|
||||||
"target": "newTodo",
|
"target": "newTodo",
|
||||||
"compute": "updateNewTodo"
|
"expression": "event.target.value"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -212,7 +216,11 @@
|
|||||||
"id": "add-todo",
|
"id": "add-todo",
|
||||||
"type": "create",
|
"type": "create",
|
||||||
"target": "todos",
|
"target": "todos",
|
||||||
"compute": "computeAddTodo"
|
"valueTemplate": {
|
||||||
|
"id": "Date.now()",
|
||||||
|
"text": "data.newTodo",
|
||||||
|
"completed": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "clear-input",
|
"id": "clear-input",
|
||||||
@@ -227,7 +235,7 @@
|
|||||||
"variant": "success"
|
"variant": "success"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"condition": "checkCanAddTodo"
|
"condition": "data.newTodo.length > 0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export interface DataSource {
|
|||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
key?: string
|
key?: string
|
||||||
defaultValue?: any
|
defaultValue?: any
|
||||||
compute?: (data: Record<string, any>) => any
|
expression?: string
|
||||||
|
valueTemplate?: Record<string, any>
|
||||||
dependencies?: string[]
|
dependencies?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +64,6 @@ export interface Action {
|
|||||||
path?: string
|
path?: string
|
||||||
value?: any
|
value?: any
|
||||||
params?: Record<string, 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")
|
// New: JSON-friendly expression (e.g., "event.target.value", "data.fieldName")
|
||||||
expression?: string
|
expression?: string
|
||||||
// New: JSON template with dynamic values
|
// New: JSON template with dynamic values
|
||||||
@@ -77,20 +76,20 @@ export interface Binding {
|
|||||||
source: string
|
source: string
|
||||||
sourceType?: BindingSourceType
|
sourceType?: BindingSourceType
|
||||||
path?: string
|
path?: string
|
||||||
transform?: string | ((value: any) => any)
|
transform?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventHandler {
|
export interface EventHandler {
|
||||||
event: string
|
event: string
|
||||||
actions: Action[]
|
actions: Action[]
|
||||||
condition?: string | ((data: Record<string, any>) => boolean)
|
condition?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JSONEventDefinition {
|
export interface JSONEventDefinition {
|
||||||
action?: string
|
action?: string
|
||||||
actions?: Action[]
|
actions?: Action[]
|
||||||
payload?: Record<string, any>
|
payload?: Record<string, any>
|
||||||
condition?: string | ((data: Record<string, any>) => boolean)
|
condition?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
|
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
|
import { PageSchema } from './json-ui'
|
||||||
import { PropConfig } from './prop-config'
|
import { PropConfig } from './prop-config'
|
||||||
import { ResizableConfig } from './resizable-config'
|
import { ResizableConfig } from './resizable-config'
|
||||||
|
|
||||||
export interface PageConfig {
|
export interface BasePageConfig {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
icon: string
|
icon: string
|
||||||
type?: 'component' | 'json'
|
|
||||||
component?: string
|
|
||||||
schemaPath?: string
|
|
||||||
schema?: string
|
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
toggleKey?: string
|
toggleKey?: string
|
||||||
@@ -18,3 +15,19 @@ export interface PageConfig {
|
|||||||
props?: PropConfig
|
props?: PropConfig
|
||||||
resizableConfig?: ResizableConfig
|
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,7 +30,8 @@ export const DataSourceSchema = z.object({
|
|||||||
key: z.string().optional(),
|
key: z.string().optional(),
|
||||||
defaultValue: z.any().optional(),
|
defaultValue: z.any().optional(),
|
||||||
dependencies: z.array(z.string()).optional(),
|
dependencies: z.array(z.string()).optional(),
|
||||||
compute: z.string().optional(),
|
expression: z.string().optional(),
|
||||||
|
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ActionConfigSchema = z.object({
|
export const ActionConfigSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user