mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 22:25:01 +00:00
Compare commits
54 Commits
codex/chan
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0dfd9e7c | ||
|
|
96744a6ab4 | ||
| f181bb870a | |||
| 05d9034366 | |||
| 29d59ec863 | |||
| 8841b74027 | |||
| d351f05b14 | |||
| 85fb859131 | |||
| d408ceff79 | |||
| b8dc6f38e6 | |||
| 73959e3d48 | |||
| d20609ecbd | |||
| 4cb9c01748 | |||
| 862e676296 | |||
| 32dd4d0eac | |||
| b34e45067d | |||
| 1a928a29dc | |||
| 27dfebcb24 | |||
| 03cc955d20 | |||
| 8c11895fba | |||
| 82b64785bf | |||
| aea8676a33 | |||
| 6abf9f8414 | |||
| ee7bc50881 | |||
| f186d67d20 | |||
| 0c375283ed | |||
| 7544c5c2e5 | |||
| 5d95bc428b | |||
| fdd1828fda | |||
| c3a05607ba | |||
| 6c777ed47c | |||
| ce9fcaf3d1 | |||
| bda28a71e4 | |||
| 4eb4849d57 | |||
| e098b9184b | |||
| b931164c3a | |||
| 7d75c6adc0 | |||
| 33e49b3671 | |||
| ace40f7e73 | |||
| 140fe351f8 | |||
| 714fb510ab | |||
| 9c3cc81c35 | |||
| def3259178 | |||
| 51040a23b9 | |||
| 785d6afc40 | |||
| 0a0046c2f3 | |||
| a0d65352a9 | |||
| baf5001704 | |||
| e075908a15 | |||
| 20f116d623 | |||
| eb9174c80d | |||
| cd9e65d4d2 | |||
| b646b8993f | |||
| f07bd37b7d |
@@ -12,7 +12,69 @@
|
|||||||
"data": "Data display and visualization components",
|
"data": "Data display and visualization components",
|
||||||
"custom": "Custom domain-specific components"
|
"custom": "Custom domain-specific components"
|
||||||
},
|
},
|
||||||
|
"sourceRoots": {
|
||||||
|
"atoms": ["@/components/atoms/*.tsx"],
|
||||||
|
"molecules": ["@/components/molecules/*.tsx"],
|
||||||
|
"organisms": ["@/components/organisms/*.tsx"],
|
||||||
|
"ui": ["@/components/ui/**/*.{ts,tsx}"],
|
||||||
|
"wrappers": ["@/lib/json-ui/wrappers/*.tsx"],
|
||||||
|
"icons": []
|
||||||
|
},
|
||||||
"components": [
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "div",
|
||||||
|
"name": "div",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Generic block container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"name": "section",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Semantic section container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "article",
|
||||||
|
"name": "article",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Semantic article container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "header",
|
||||||
|
"name": "header",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Semantic header container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "footer",
|
||||||
|
"name": "footer",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Semantic footer container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "main",
|
||||||
|
"name": "main",
|
||||||
|
"category": "layout",
|
||||||
|
"canHaveChildren": true,
|
||||||
|
"description": "Semantic main container",
|
||||||
|
"status": "supported",
|
||||||
|
"source": "primitive"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "ActionCard",
|
"type": "ActionCard",
|
||||||
"name": "ActionCard",
|
"name": "ActionCard",
|
||||||
@@ -80,7 +142,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "ComponentBindingDialog"
|
"wrapperFor": "ComponentBindingDialog",
|
||||||
|
"load": {
|
||||||
|
"export": "ComponentBindingDialogWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Container",
|
"type": "Container",
|
||||||
@@ -122,7 +187,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "DataSourceEditorDialog"
|
"wrapperFor": "DataSourceEditorDialog",
|
||||||
|
"load": {
|
||||||
|
"export": "DataSourceEditorDialogWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Dialog",
|
"type": "Dialog",
|
||||||
@@ -724,7 +792,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ArrowLeft icon",
|
"description": "ArrowLeft icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "ArrowLeft"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ArrowRight",
|
"type": "ArrowRight",
|
||||||
@@ -733,7 +804,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ArrowRight icon",
|
"description": "ArrowRight icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "ArrowRight"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Check",
|
"type": "Check",
|
||||||
@@ -742,7 +816,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Check icon",
|
"description": "Check icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Check"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "X",
|
"type": "X",
|
||||||
@@ -751,7 +828,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "X icon",
|
"description": "X icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "X"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Plus",
|
"type": "Plus",
|
||||||
@@ -760,7 +840,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Plus icon",
|
"description": "Plus icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Plus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Minus",
|
"type": "Minus",
|
||||||
@@ -769,7 +852,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Minus icon",
|
"description": "Minus icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Minus"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Search",
|
"type": "Search",
|
||||||
@@ -778,7 +864,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Search icon",
|
"description": "Search icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "MagnifyingGlass"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Filter",
|
"type": "Filter",
|
||||||
@@ -787,7 +876,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Filter icon",
|
"description": "Filter icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Funnel"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Download",
|
"type": "Download",
|
||||||
@@ -796,7 +888,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Download icon",
|
"description": "Download icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Download"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Upload",
|
"type": "Upload",
|
||||||
@@ -805,7 +900,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Upload icon",
|
"description": "Upload icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Upload"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Edit",
|
"type": "Edit",
|
||||||
@@ -814,7 +912,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Edit icon",
|
"description": "Edit icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "PencilSimple"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Trash",
|
"type": "Trash",
|
||||||
@@ -823,7 +924,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Trash icon",
|
"description": "Trash icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Trash"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Eye",
|
"type": "Eye",
|
||||||
@@ -832,7 +936,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Eye icon",
|
"description": "Eye icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Eye"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "EyeOff",
|
"type": "EyeOff",
|
||||||
@@ -841,7 +948,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "EyeOff icon",
|
"description": "EyeOff icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "EyeClosed"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ChevronUp",
|
"type": "ChevronUp",
|
||||||
@@ -850,7 +960,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ChevronUp icon",
|
"description": "ChevronUp icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "CaretUp"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ChevronDown",
|
"type": "ChevronDown",
|
||||||
@@ -859,7 +972,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ChevronDown icon",
|
"description": "ChevronDown icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "CaretDown"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ChevronLeft",
|
"type": "ChevronLeft",
|
||||||
@@ -868,7 +984,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ChevronLeft icon",
|
"description": "ChevronLeft icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "CaretLeft"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ChevronRight",
|
"type": "ChevronRight",
|
||||||
@@ -877,7 +996,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "ChevronRight icon",
|
"description": "ChevronRight icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "CaretRight"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Settings",
|
"type": "Settings",
|
||||||
@@ -886,7 +1008,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Settings icon",
|
"description": "Settings icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Gear"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "User",
|
"type": "User",
|
||||||
@@ -895,7 +1020,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "User icon",
|
"description": "User icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "User"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Bell",
|
"type": "Bell",
|
||||||
@@ -904,7 +1032,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Bell icon",
|
"description": "Bell icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Bell"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Mail",
|
"type": "Mail",
|
||||||
@@ -913,7 +1044,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Mail icon",
|
"description": "Mail icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Envelope"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Calendar",
|
"type": "Calendar",
|
||||||
@@ -922,7 +1056,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Calendar icon",
|
"description": "Calendar icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Calendar"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Clock",
|
"type": "Clock",
|
||||||
@@ -931,7 +1068,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Clock icon",
|
"description": "Clock icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Clock"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Star",
|
"type": "Star",
|
||||||
@@ -940,7 +1080,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Star icon",
|
"description": "Star icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Star"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Heart",
|
"type": "Heart",
|
||||||
@@ -949,7 +1092,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Heart icon",
|
"description": "Heart icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Heart"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Share",
|
"type": "Share",
|
||||||
@@ -958,7 +1104,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Share icon",
|
"description": "Share icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "ShareNetwork"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Link",
|
"type": "Link",
|
||||||
@@ -967,7 +1116,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Link icon",
|
"description": "Link icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "LinkSimple"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Copy",
|
"type": "Copy",
|
||||||
@@ -976,7 +1128,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Copy icon",
|
"description": "Copy icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Copy"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Save",
|
"type": "Save",
|
||||||
@@ -985,7 +1140,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Save icon",
|
"description": "Save icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "FloppyDisk"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "RefreshCw",
|
"type": "RefreshCw",
|
||||||
@@ -994,7 +1152,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "RefreshCw icon",
|
"description": "RefreshCw icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "ArrowClockwise"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "AlertCircle",
|
"type": "AlertCircle",
|
||||||
@@ -1003,7 +1164,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "AlertCircle icon",
|
"description": "AlertCircle icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "WarningCircle"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Info",
|
"type": "Info",
|
||||||
@@ -1012,7 +1176,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Info icon",
|
"description": "Info icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Info"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "HelpCircle",
|
"type": "HelpCircle",
|
||||||
@@ -1021,7 +1188,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "HelpCircle icon",
|
"description": "HelpCircle icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "Question"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Home",
|
"type": "Home",
|
||||||
@@ -1030,7 +1200,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Home icon",
|
"description": "Home icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "House"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Menu",
|
"type": "Menu",
|
||||||
@@ -1039,7 +1212,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Menu icon",
|
"description": "Menu icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "List"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "MoreVertical",
|
"type": "MoreVertical",
|
||||||
@@ -1048,7 +1224,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "MoreVertical icon",
|
"description": "MoreVertical icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "DotsThreeVertical"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "MoreHorizontal",
|
"type": "MoreHorizontal",
|
||||||
@@ -1057,7 +1236,10 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "MoreHorizontal icon",
|
"description": "MoreHorizontal icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "icons"
|
"source": "icons",
|
||||||
|
"load": {
|
||||||
|
"export": "DotsThree"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Breadcrumb",
|
"type": "Breadcrumb",
|
||||||
@@ -1275,7 +1457,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "GitHubBuildStatus"
|
"wrapperFor": "GitHubBuildStatus",
|
||||||
|
"load": {
|
||||||
|
"export": "GitHubBuildStatusWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "InfoBox",
|
"type": "InfoBox",
|
||||||
@@ -1377,7 +1562,11 @@
|
|||||||
"canHaveChildren": true,
|
"canHaveChildren": true,
|
||||||
"description": "Chart component",
|
"description": "Chart component",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "ui"
|
"source": "ui",
|
||||||
|
"load": {
|
||||||
|
"path": "@/components/ui/chart/chart-container.tsx",
|
||||||
|
"export": "ChartContainer"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "DataList",
|
"type": "DataList",
|
||||||
@@ -1437,7 +1626,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "LazyBarChart"
|
"wrapperFor": "LazyBarChart",
|
||||||
|
"load": {
|
||||||
|
"export": "LazyBarChartWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "LazyD3BarChart",
|
"type": "LazyD3BarChart",
|
||||||
@@ -1460,7 +1652,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "LazyD3BarChart"
|
"wrapperFor": "LazyD3BarChart",
|
||||||
|
"load": {
|
||||||
|
"export": "LazyD3BarChartWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "LazyLineChart",
|
"type": "LazyLineChart",
|
||||||
@@ -1483,7 +1678,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "LazyLineChart"
|
"wrapperFor": "LazyLineChart",
|
||||||
|
"load": {
|
||||||
|
"export": "LazyLineChartWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "List",
|
"type": "List",
|
||||||
@@ -1542,7 +1740,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "SeedDataManager"
|
"wrapperFor": "SeedDataManager",
|
||||||
|
"load": {
|
||||||
|
"export": "SeedDataManagerWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "StatCard",
|
"type": "StatCard",
|
||||||
@@ -1829,7 +2030,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "ComponentTree"
|
"wrapperFor": "ComponentTree",
|
||||||
|
"load": {
|
||||||
|
"export": "ComponentTreeWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ComponentTreeNode",
|
"type": "ComponentTreeNode",
|
||||||
@@ -1909,7 +2113,11 @@
|
|||||||
"description": "JSONUIShowcase organism component",
|
"description": "JSONUIShowcase organism component",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "organisms",
|
"source": "organisms",
|
||||||
"jsonCompatible": true
|
"jsonCompatible": true,
|
||||||
|
"load": {
|
||||||
|
"path": "@/components/JSONUIShowcase.tsx",
|
||||||
|
"export": "JSONUIShowcase"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Kbd",
|
"type": "Kbd",
|
||||||
@@ -1966,7 +2174,11 @@
|
|||||||
"canHaveChildren": true,
|
"canHaveChildren": true,
|
||||||
"description": "PageHeader component",
|
"description": "PageHeader component",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "atoms"
|
"source": "atoms",
|
||||||
|
"load": {
|
||||||
|
"path": "@/components/atoms/PageHeader.tsx",
|
||||||
|
"export": "BasicPageHeader"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "PageHeaderContent",
|
"type": "PageHeaderContent",
|
||||||
@@ -2049,7 +2261,11 @@
|
|||||||
"canHaveChildren": true,
|
"canHaveChildren": true,
|
||||||
"description": "Resizable component",
|
"description": "Resizable component",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "ui"
|
"source": "ui",
|
||||||
|
"load": {
|
||||||
|
"path": "@/components/ui/resizable.tsx",
|
||||||
|
"export": "ResizablePanelGroup"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SaveIndicator",
|
"type": "SaveIndicator",
|
||||||
@@ -2072,7 +2288,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "SaveIndicator"
|
"wrapperFor": "SaveIndicator",
|
||||||
|
"load": {
|
||||||
|
"export": "SaveIndicatorWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SchemaEditorCanvas",
|
"type": "SchemaEditorCanvas",
|
||||||
@@ -2150,7 +2369,11 @@
|
|||||||
"canHaveChildren": false,
|
"canHaveChildren": false,
|
||||||
"description": "Search input with icon",
|
"description": "Search input with icon",
|
||||||
"status": "supported",
|
"status": "supported",
|
||||||
"source": "atoms"
|
"source": "atoms",
|
||||||
|
"load": {
|
||||||
|
"path": "@/components/atoms/SearchInput.tsx",
|
||||||
|
"export": "BasicSearchInput"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Sheet",
|
"type": "Sheet",
|
||||||
@@ -2236,7 +2459,10 @@
|
|||||||
"status": "json-compatible",
|
"status": "json-compatible",
|
||||||
"source": "wrappers",
|
"source": "wrappers",
|
||||||
"jsonCompatible": true,
|
"jsonCompatible": true,
|
||||||
"wrapperFor": "StorageSettings"
|
"wrapperFor": "StorageSettings",
|
||||||
|
"load": {
|
||||||
|
"export": "StorageSettingsWrapper"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Timestamp",
|
"type": "Timestamp",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"kill": "fuser -k 5000/tcp",
|
"kill": "fuser -k 5000/tcp",
|
||||||
"prebuild": "mkdir -p /tmp/dist || true",
|
"predev": "npm run components:generate-types",
|
||||||
|
"prebuild": "npm run components:generate-types && mkdir -p /tmp/dist || true",
|
||||||
"build": "tsc -b --noCheck && vite build",
|
"build": "tsc -b --noCheck && vite build",
|
||||||
"lint": "eslint . --fix && npm run lint:schemas",
|
"lint": "eslint . --fix && npm run lint:schemas",
|
||||||
"lint:check": "eslint . && npm run lint:schemas",
|
"lint:check": "eslint . && npm run lint:schemas",
|
||||||
@@ -24,8 +25,9 @@
|
|||||||
"pages:generate": "node scripts/generate-page.js",
|
"pages:generate": "node scripts/generate-page.js",
|
||||||
"schemas:validate": "tsx scripts/validate-json-schemas.ts",
|
"schemas:validate": "tsx scripts/validate-json-schemas.ts",
|
||||||
"components:list": "node scripts/list-json-components.cjs",
|
"components:list": "node scripts/list-json-components.cjs",
|
||||||
|
"components:generate-types": "tsx scripts/generate-json-ui-component-types.ts",
|
||||||
"components:scan": "node scripts/scan-and-update-registry.cjs",
|
"components:scan": "node scripts/scan-and-update-registry.cjs",
|
||||||
"components:validate": "node scripts/validate-supported-components.cjs"
|
"components:validate": "node scripts/validate-supported-components.cjs && tsx scripts/validate-json-registry.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
|||||||
@@ -39,9 +39,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "trends",
|
"id": "trends",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => ({ filesGrowth: 12, modelsGrowth: -3, componentsGrowth: 8, testsGrowth: 15 })",
|
"defaultValue": {
|
||||||
"dependencies": ["metrics"]
|
"filesGrowth": 12,
|
||||||
|
"modelsGrowth": -3,
|
||||||
|
"componentsGrowth": 8,
|
||||||
|
"testsGrowth": 15
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -25,9 +25,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filteredFiles",
|
"id": "filteredFiles",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => {\n if (!data.searchQuery) return data.files;\n return data.files.filter(f => f.name.toLowerCase().includes(data.searchQuery.toLowerCase()));\n}",
|
"expression": "data.files",
|
||||||
"dependencies": ["files", "searchQuery"]
|
"dependencies": [
|
||||||
|
"files",
|
||||||
|
"searchQuery"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -22,6 +22,15 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sourceRoots": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -73,6 +82,19 @@
|
|||||||
"wrapperFor": {
|
"wrapperFor": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"load": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["export"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"deprecated": {
|
"deprecated": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
50
scripts/generate-json-ui-component-types.ts
Normal file
50
scripts/generate-json-ui-component-types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
interface RegistryComponent {
|
||||||
|
type?: string
|
||||||
|
name?: string
|
||||||
|
export?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegistryData {
|
||||||
|
components?: RegistryComponent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
const registryPath = path.join(rootDir, 'json-components-registry.json')
|
||||||
|
const outputPath = path.join(rootDir, 'src/types/json-ui-component-types.ts')
|
||||||
|
|
||||||
|
const registryData = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as RegistryData
|
||||||
|
const components = registryData.components ?? []
|
||||||
|
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const componentTypes = components.flatMap((component) => {
|
||||||
|
const typeName = component.type ?? component.name ?? component.export
|
||||||
|
if (!typeName || typeof typeName !== 'string') {
|
||||||
|
throw new Error('Registry component is missing a valid type/name/export entry.')
|
||||||
|
}
|
||||||
|
if (seen.has(typeName)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
seen.add(typeName)
|
||||||
|
return [typeName]
|
||||||
|
})
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'// This file is auto-generated by scripts/generate-json-ui-component-types.ts.',
|
||||||
|
'// Do not edit this file directly.',
|
||||||
|
'',
|
||||||
|
'export const jsonUIComponentTypes = [',
|
||||||
|
...componentTypes.map((typeName) => ` ${JSON.stringify(typeName)},`),
|
||||||
|
'] as const',
|
||||||
|
'',
|
||||||
|
'export type JSONUIComponentType = typeof jsonUIComponentTypes[number]',
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, `${lines.join('\n')}`)
|
||||||
|
|
||||||
|
console.log(`✅ Wrote ${componentTypes.length} component types to ${outputPath}`)
|
||||||
235
scripts/validate-json-registry.ts
Normal file
235
scripts/validate-json-registry.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||||
|
import * as PhosphorIcons from '@phosphor-icons/react'
|
||||||
|
import { JSONUIShowcase } from '../src/components/JSONUIShowcase'
|
||||||
|
|
||||||
|
type ComponentType = unknown
|
||||||
|
|
||||||
|
interface JsonRegistryEntry {
|
||||||
|
name?: string
|
||||||
|
type?: string
|
||||||
|
export?: string
|
||||||
|
source?: string
|
||||||
|
status?: string
|
||||||
|
wrapperRequired?: boolean
|
||||||
|
wrapperComponent?: string
|
||||||
|
wrapperFor?: string
|
||||||
|
load?: {
|
||||||
|
export?: string
|
||||||
|
}
|
||||||
|
deprecated?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonComponentRegistry {
|
||||||
|
components?: JsonRegistryEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceAliases: Record<string, Record<string, string>> = {
|
||||||
|
atoms: {
|
||||||
|
PageHeader: 'BasicPageHeader',
|
||||||
|
SearchInput: 'BasicSearchInput',
|
||||||
|
},
|
||||||
|
molecules: {},
|
||||||
|
organisms: {},
|
||||||
|
ui: {
|
||||||
|
Chart: 'ChartContainer',
|
||||||
|
Resizable: 'ResizablePanelGroup',
|
||||||
|
},
|
||||||
|
wrappers: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitComponentAllowlist: Record<string, ComponentType> = {
|
||||||
|
JSONUIShowcase,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRegistryEntryKey = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.name ?? entry.type
|
||||||
|
|
||||||
|
const getRegistryEntryExportName = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.load?.export ?? entry.export ?? getRegistryEntryKey(entry)
|
||||||
|
|
||||||
|
const buildComponentMapFromExports = (
|
||||||
|
exports: Record<string, unknown>
|
||||||
|
): Record<string, ComponentType> => {
|
||||||
|
return Object.entries(exports).reduce<Record<string, ComponentType>>((acc, [key, value]) => {
|
||||||
|
if (value && (typeof value === 'function' || typeof value === 'object')) {
|
||||||
|
acc[key] = value as ComponentType
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildComponentMapFromModules = (
|
||||||
|
modules: Record<string, unknown>
|
||||||
|
): Record<string, ComponentType> => {
|
||||||
|
return Object.values(modules).reduce<Record<string, ComponentType>>((acc, moduleExports) => {
|
||||||
|
if (!moduleExports || typeof moduleExports !== 'object') {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
Object.entries(buildComponentMapFromExports(moduleExports as Record<string, unknown>)).forEach(
|
||||||
|
([key, component]) => {
|
||||||
|
acc[key] = component
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const listFiles = async (options: {
|
||||||
|
directory: string
|
||||||
|
extensions: string[]
|
||||||
|
recursive: boolean
|
||||||
|
}): Promise<string[]> => {
|
||||||
|
const { directory, extensions, recursive } = options
|
||||||
|
const entries = await fs.readdir(directory, { withFileTypes: true })
|
||||||
|
const files: string[] = []
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
entries.map(async (entry) => {
|
||||||
|
const fullPath = path.join(directory, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (recursive) {
|
||||||
|
const nested = await listFiles({ directory: fullPath, extensions, recursive })
|
||||||
|
files.push(...nested)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (extensions.includes(path.extname(entry.name))) {
|
||||||
|
files.push(fullPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
const importModules = async (files: string[]): Promise<Record<string, unknown>> => {
|
||||||
|
const modules: Record<string, unknown> = {}
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (file) => {
|
||||||
|
const moduleExports = await import(pathToFileURL(file).href)
|
||||||
|
modules[file] = moduleExports
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRegistry = async () => {
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(scriptDir, '..')
|
||||||
|
const registryPath = path.join(rootDir, 'json-components-registry.json')
|
||||||
|
|
||||||
|
const registryRaw = await fs.readFile(registryPath, 'utf8')
|
||||||
|
const registry = JSON.parse(registryRaw) as JsonComponentRegistry
|
||||||
|
const registryEntries = registry.components ?? []
|
||||||
|
const registryEntryByType = new Map(
|
||||||
|
registryEntries
|
||||||
|
.map((entry) => {
|
||||||
|
const entryKey = getRegistryEntryKey(entry)
|
||||||
|
return entryKey ? [entryKey, entry] : null
|
||||||
|
})
|
||||||
|
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
|
||||||
|
)
|
||||||
|
|
||||||
|
const sourceConfigs = [
|
||||||
|
{
|
||||||
|
source: 'atoms',
|
||||||
|
directory: path.join(rootDir, 'src/components/atoms'),
|
||||||
|
extensions: ['.tsx'],
|
||||||
|
recursive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'molecules',
|
||||||
|
directory: path.join(rootDir, 'src/components/molecules'),
|
||||||
|
extensions: ['.tsx'],
|
||||||
|
recursive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'organisms',
|
||||||
|
directory: path.join(rootDir, 'src/components/organisms'),
|
||||||
|
extensions: ['.tsx'],
|
||||||
|
recursive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'ui',
|
||||||
|
directory: path.join(rootDir, 'src/components/ui'),
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
recursive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'wrappers',
|
||||||
|
directory: path.join(rootDir, 'src/lib/json-ui/wrappers'),
|
||||||
|
extensions: ['.tsx'],
|
||||||
|
recursive: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const componentMaps: Record<string, Record<string, ComponentType>> = {}
|
||||||
|
await Promise.all(
|
||||||
|
sourceConfigs.map(async (config) => {
|
||||||
|
const files = await listFiles({
|
||||||
|
directory: config.directory,
|
||||||
|
extensions: config.extensions,
|
||||||
|
recursive: config.recursive,
|
||||||
|
})
|
||||||
|
const modules = await importModules(files)
|
||||||
|
componentMaps[config.source] = buildComponentMapFromModules(modules)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
componentMaps.icons = buildComponentMapFromExports(PhosphorIcons)
|
||||||
|
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
registryEntries.forEach((entry) => {
|
||||||
|
const entryKey = getRegistryEntryKey(entry)
|
||||||
|
const entryExportName = getRegistryEntryExportName(entry)
|
||||||
|
|
||||||
|
if (!entryKey || !entryExportName) {
|
||||||
|
errors.push(`Entry missing name/type/export: ${JSON.stringify(entry)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = entry.source
|
||||||
|
if (!source || !componentMaps[source]) {
|
||||||
|
errors.push(`${entryKey}: unknown source "${source ?? 'missing'}"`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasName = sourceAliases[source]?.[entryKey]
|
||||||
|
const component =
|
||||||
|
componentMaps[source][entryExportName] ??
|
||||||
|
(aliasName ? componentMaps[source][aliasName] : undefined) ??
|
||||||
|
explicitComponentAllowlist[entryKey]
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
const aliasNote = aliasName ? ` (alias: ${aliasName})` : ''
|
||||||
|
errors.push(
|
||||||
|
`${entryKey} (${source}) did not resolve export "${entryExportName}"${aliasNote}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.wrapperRequired) {
|
||||||
|
if (!entry.wrapperComponent) {
|
||||||
|
errors.push(`${entryKey} (${source}) requires a wrapperComponent but none is defined`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!registryEntryByType.has(entry.wrapperComponent)) {
|
||||||
|
errors.push(
|
||||||
|
`${entryKey} (${source}) references missing wrapperComponent ${entry.wrapperComponent}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error('❌ JSON component registry export validation failed:')
|
||||||
|
errors.forEach((error) => console.error(`- ${error}`))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ JSON component registry exports are valid.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateRegistry()
|
||||||
@@ -4,7 +4,7 @@ const path = require('path')
|
|||||||
const rootDir = path.resolve(__dirname, '..')
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
const registryPath = path.join(rootDir, 'json-components-registry.json')
|
const registryPath = path.join(rootDir, 'json-components-registry.json')
|
||||||
const definitionsPath = path.join(rootDir, 'src/lib/component-definitions.json')
|
const definitionsPath = path.join(rootDir, 'src/lib/component-definitions.json')
|
||||||
const componentTypesPath = path.join(rootDir, 'src/types/json-ui.ts')
|
const componentTypesPath = path.join(rootDir, 'src/types/json-ui-component-types.ts')
|
||||||
const uiRegistryPath = path.join(rootDir, 'src/lib/json-ui/component-registry.ts')
|
const uiRegistryPath = path.join(rootDir, 'src/lib/json-ui/component-registry.ts')
|
||||||
const atomIndexPath = path.join(rootDir, 'src/components/atoms/index.ts')
|
const atomIndexPath = path.join(rootDir, 'src/components/atoms/index.ts')
|
||||||
const moleculeIndexPath = path.join(rootDir, 'src/components/molecules/index.ts')
|
const moleculeIndexPath = path.join(rootDir, 'src/components/molecules/index.ts')
|
||||||
@@ -21,16 +21,10 @@ const componentDefinitions = readJson(definitionsPath)
|
|||||||
const definitionTypes = new Set(componentDefinitions.map((def) => def.type))
|
const definitionTypes = new Set(componentDefinitions.map((def) => def.type))
|
||||||
|
|
||||||
const componentTypesContent = readText(componentTypesPath)
|
const componentTypesContent = readText(componentTypesPath)
|
||||||
const componentTypesStart = componentTypesContent.indexOf('export type ComponentType')
|
|
||||||
const componentTypesEnd = componentTypesContent.indexOf('export type ActionType')
|
|
||||||
if (componentTypesStart === -1 || componentTypesEnd === -1) {
|
|
||||||
throw new Error('Unable to locate ComponentType union in src/types/json-ui.ts')
|
|
||||||
}
|
|
||||||
const componentTypesBlock = componentTypesContent.slice(componentTypesStart, componentTypesEnd)
|
|
||||||
const componentTypeSet = new Set()
|
const componentTypeSet = new Set()
|
||||||
const componentTypeRegex = /'([^']+)'/g
|
const componentTypeRegex = /"([^"]+)"/g
|
||||||
let match
|
let match
|
||||||
while ((match = componentTypeRegex.exec(componentTypesBlock)) !== null) {
|
while ((match = componentTypeRegex.exec(componentTypesContent)) !== null) {
|
||||||
componentTypeSet.add(match[1])
|
componentTypeSet.add(match[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,153 +1,64 @@
|
|||||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { FeatureToggles } from '@/types/project'
|
import { FeatureToggles } from '@/types/project'
|
||||||
import {
|
import { useMemo } from 'react'
|
||||||
BookOpen,
|
import featureToggleSchema from '@/schemas/feature-toggle-settings.json'
|
||||||
Code,
|
import type { PageSchema } from '@/types/json-ui'
|
||||||
Cube,
|
import { evaluateExpression } from '@/lib/json-ui/expression-evaluator'
|
||||||
Database,
|
|
||||||
FileText,
|
|
||||||
Flask,
|
|
||||||
FlowArrow,
|
|
||||||
Image,
|
|
||||||
Lightbulb,
|
|
||||||
PaintBrush,
|
|
||||||
Play,
|
|
||||||
Tree,
|
|
||||||
Wrench,
|
|
||||||
} from '@phosphor-icons/react'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import featureToggleSettings from '@/config/feature-toggle-settings.json'
|
|
||||||
import type { ComponentType } from 'react'
|
|
||||||
|
|
||||||
interface FeatureToggleSettingsProps {
|
interface FeatureToggleSettingsProps {
|
||||||
features: FeatureToggles
|
features: FeatureToggles
|
||||||
onFeaturesChange: (features: FeatureToggles) => void
|
onFeaturesChange: (features: FeatureToggles) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureToggleIconKey =
|
/**
|
||||||
| 'BookOpen'
|
* FeatureToggleSettings - Now JSON-driven!
|
||||||
| 'Code'
|
*
|
||||||
| 'Cube'
|
* This component demonstrates how a complex React component with:
|
||||||
| 'Database'
|
* - Custom hooks and state management
|
||||||
| 'FileText'
|
* - Dynamic data rendering (looping over features)
|
||||||
| 'Flask'
|
* - Event handlers (toggle switches)
|
||||||
| 'FlowArrow'
|
* - Conditional styling (enabled/disabled states)
|
||||||
| 'Image'
|
*
|
||||||
| 'Lightbulb'
|
* Can be converted to a pure JSON schema with custom action handlers.
|
||||||
| 'PaintBrush'
|
* The JSON schema handles all UI structure, data binding, and loops,
|
||||||
| 'Play'
|
* while custom functions handle business logic.
|
||||||
| 'Tree'
|
*
|
||||||
| 'Wrench'
|
* Converted from 153 lines of React/TSX to:
|
||||||
|
* - 1 JSON schema file (195 lines, but mostly structure)
|
||||||
const iconMap: Record<FeatureToggleIconKey, ComponentType<{ size?: number; weight?: 'duotone' }>> = {
|
* - 45 lines of integration code (this file)
|
||||||
BookOpen,
|
*
|
||||||
Code,
|
* Benefits:
|
||||||
Cube,
|
* - UI structure is now data-driven and can be modified without code changes
|
||||||
Database,
|
* - Feature list is in JSON and can be easily extended
|
||||||
FileText,
|
* - Styling and layout can be customized via JSON
|
||||||
Flask,
|
* - Business logic (toggle handler) stays in TypeScript for type safety
|
||||||
FlowArrow,
|
*/
|
||||||
Image,
|
|
||||||
Lightbulb,
|
|
||||||
PaintBrush,
|
|
||||||
Play,
|
|
||||||
Tree,
|
|
||||||
Wrench,
|
|
||||||
}
|
|
||||||
|
|
||||||
type FeatureToggleItem = {
|
|
||||||
key: keyof FeatureToggles
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
icon: FeatureToggleIconKey
|
|
||||||
}
|
|
||||||
|
|
||||||
const featuresList = featureToggleSettings as FeatureToggleItem[]
|
|
||||||
|
|
||||||
function FeatureToggleHeader({ enabledCount, totalCount }: { enabledCount: number; totalCount: number }) {
|
|
||||||
return (
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-2">Feature Toggles</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Enable or disable features to customize your workspace. {enabledCount} of {totalCount} features enabled.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FeatureToggleCard({
|
|
||||||
item,
|
|
||||||
enabled,
|
|
||||||
onToggle,
|
|
||||||
}: {
|
|
||||||
item: FeatureToggleItem
|
|
||||||
enabled: boolean
|
|
||||||
onToggle: (value: boolean) => void
|
|
||||||
}) {
|
|
||||||
const Icon = iconMap[item.icon]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className={`p-2 rounded-lg ${enabled ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
|
|
||||||
<Icon size={20} weight="duotone" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-base">{item.label}</CardTitle>
|
|
||||||
<CardDescription className="text-xs mt-1">{item.description}</CardDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch id={item.key} checked={enabled} onCheckedChange={onToggle} />
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FeatureToggleGrid({
|
|
||||||
items,
|
|
||||||
features,
|
|
||||||
onToggle,
|
|
||||||
}: {
|
|
||||||
items: FeatureToggleItem[]
|
|
||||||
features: FeatureToggles
|
|
||||||
onToggle: (key: keyof FeatureToggles, value: boolean) => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pr-4">
|
|
||||||
{items.map((item) => (
|
|
||||||
<FeatureToggleCard
|
|
||||||
key={item.key}
|
|
||||||
item={item}
|
|
||||||
enabled={features[item.key]}
|
|
||||||
onToggle={(checked) => onToggle(item.key, checked)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeatureToggleSettings({ features, onFeaturesChange }: FeatureToggleSettingsProps) {
|
export function FeatureToggleSettings({ features, onFeaturesChange }: FeatureToggleSettingsProps) {
|
||||||
const handleToggle = (key: keyof FeatureToggles, value: boolean) => {
|
// Custom action handler - this is the "hook" that handles complex logic
|
||||||
onFeaturesChange({
|
const handlers = useMemo(() => ({
|
||||||
...features,
|
updateFeature: (action: any, eventData: any) => {
|
||||||
[key]: value,
|
// Evaluate the params to get the actual values
|
||||||
})
|
const context = { data: { features, item: eventData.item }, event: eventData }
|
||||||
}
|
|
||||||
|
// The key param is an expression like "item.key" which needs evaluation
|
||||||
|
const key = evaluateExpression(action.params.key, context) as keyof FeatureToggles
|
||||||
|
const checked = eventData as boolean
|
||||||
|
|
||||||
|
onFeaturesChange({
|
||||||
|
...features,
|
||||||
|
[key]: checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}), [features, onFeaturesChange])
|
||||||
|
|
||||||
const enabledCount = Object.values(features).filter(Boolean).length
|
// Pass features as external data to the JSON renderer
|
||||||
const totalCount = Object.keys(features).length
|
const data = useMemo(() => ({ features }), [features])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full p-6 bg-background">
|
<PageRenderer
|
||||||
<FeatureToggleHeader enabledCount={enabledCount} totalCount={totalCount} />
|
schema={featureToggleSchema as PageSchema}
|
||||||
|
data={data}
|
||||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
functions={handlers}
|
||||||
<FeatureToggleGrid items={featuresList} features={features} onToggle={handleToggle} />
|
/>
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import showcaseCopy from '@/config/ui-examples/showcase.json'
|
import showcaseCopy from '@/config/ui-examples/showcase.json'
|
||||||
import dashboardExample from '@/config/ui-examples/dashboard.json'
|
|
||||||
import formExample from '@/config/ui-examples/form.json'
|
|
||||||
import tableExample from '@/config/ui-examples/table.json'
|
|
||||||
import listTableTimelineExample from '@/config/ui-examples/list-table-timeline.json'
|
|
||||||
import settingsExample from '@/config/ui-examples/settings.json'
|
|
||||||
import { FileCode, ChartBar, ListBullets, Table, Gear, Clock } from '@phosphor-icons/react'
|
import { FileCode, ChartBar, ListBullets, Table, Gear, Clock } from '@phosphor-icons/react'
|
||||||
import { ShowcaseHeader } from '@/components/json-ui-showcase/ShowcaseHeader'
|
import { ShowcaseHeader } from '@/components/json-ui-showcase/ShowcaseHeader'
|
||||||
import { ShowcaseTabs } from '@/components/json-ui-showcase/ShowcaseTabs'
|
import { ShowcaseTabs } from '@/components/json-ui-showcase/ShowcaseTabs'
|
||||||
import { ShowcaseFooter } from '@/components/json-ui-showcase/ShowcaseFooter'
|
import { ShowcaseFooter } from '@/components/json-ui-showcase/ShowcaseFooter'
|
||||||
import { ShowcaseExample } from '@/components/json-ui-showcase/types'
|
import { ShowcaseExample } from '@/components/json-ui-showcase/types'
|
||||||
|
|
||||||
const exampleConfigs = {
|
|
||||||
dashboard: dashboardExample,
|
|
||||||
form: formExample,
|
|
||||||
table: tableExample,
|
|
||||||
'list-table-timeline': listTableTimelineExample,
|
|
||||||
settings: settingsExample,
|
|
||||||
}
|
|
||||||
|
|
||||||
const exampleIcons = {
|
const exampleIcons = {
|
||||||
ChartBar,
|
ChartBar,
|
||||||
ListBullets,
|
ListBullets,
|
||||||
@@ -27,14 +14,22 @@ const exampleIcons = {
|
|||||||
Gear,
|
Gear,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configModules = import.meta.glob('/src/config/ui-examples/*.json', { eager: true })
|
||||||
|
|
||||||
|
const resolveExampleConfig = (configPath: string) => {
|
||||||
|
const moduleEntry = configModules[configPath] as { default: ShowcaseExample['config'] } | undefined
|
||||||
|
|
||||||
|
return moduleEntry?.default ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
export function JSONUIShowcase() {
|
export function JSONUIShowcase() {
|
||||||
const [selectedExample, setSelectedExample] = useState(showcaseCopy.defaultExampleKey)
|
const [selectedExample, setSelectedExample] = useState(showcaseCopy.defaultExampleKey)
|
||||||
const [showJSON, setShowJSON] = useState(false)
|
const [showJSON, setShowJSON] = useState(false)
|
||||||
|
|
||||||
const examples = useMemo<ShowcaseExample[]>(() => {
|
const examples = useMemo<ShowcaseExample[]>(() => {
|
||||||
return showcaseCopy.examples.map((example) => {
|
return showcaseCopy.examples.map((example) => {
|
||||||
const icon = exampleIcons[example.icon as keyof typeof exampleIcons] || FileCode
|
const icon = exampleIcons[example.iconId as keyof typeof exampleIcons] || FileCode
|
||||||
const config = exampleConfigs[example.configKey as keyof typeof exampleConfigs]
|
const config = resolveExampleConfig(example.configPath)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: example.key,
|
key: example.key,
|
||||||
|
|||||||
@@ -45,11 +45,12 @@ function getCompletionMessage(score: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectDashboard(props: ProjectDashboardProps) {
|
export function ProjectDashboard(props: ProjectDashboardProps) {
|
||||||
|
const completionMetrics = calculateCompletionScore(props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JSONPageRenderer
|
<JSONPageRenderer
|
||||||
schema={dashboardSchema as any}
|
schema={dashboardSchema as any}
|
||||||
data={props}
|
data={{ ...props, ...completionMetrics }}
|
||||||
functions={{ calculateCompletionScore }}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
import { DataSourceType } from '@/types/json-ui'
|
||||||
import { Database, Function, File } from '@phosphor-icons/react'
|
import { Database, File } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceBadgeProps {
|
interface DataSourceBadgeProps {
|
||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
@@ -13,11 +13,6 @@ const dataSourceConfig = {
|
|||||||
label: 'KV Storage',
|
label: 'KV Storage',
|
||||||
className: 'bg-accent/20 text-accent border-accent/30'
|
className: 'bg-accent/20 text-accent border-accent/30'
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
icon: Function,
|
|
||||||
label: 'Computed',
|
|
||||||
className: 'bg-primary/20 text-primary border-primary/30'
|
|
||||||
},
|
|
||||||
static: {
|
static: {
|
||||||
icon: File,
|
icon: File,
|
||||||
label: 'Static',
|
label: 'Static',
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function PageCard({ card, data, functions }: PageCardProps) {
|
|||||||
|
|
||||||
if (card.type === 'gradient-card') {
|
if (card.type === 'gradient-card') {
|
||||||
const computeFn = functions[card.dataSource?.compute]
|
const computeFn = functions[card.dataSource?.compute]
|
||||||
const computedData = computeFn ? computeFn(data) : {}
|
const computedData = computeFn ? computeFn(data) : data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Card, Badge, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceCardProps {
|
interface DataSourceCardProps {
|
||||||
dataSource: DataSource
|
dataSource: DataSource
|
||||||
@@ -11,13 +11,6 @@ interface DataSourceCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
||||||
const getDependencyCount = () => {
|
|
||||||
if (dataSource.type === 'computed') {
|
|
||||||
return dataSource.dependencies?.length || 0
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderTypeSpecificInfo = () => {
|
const renderTypeSpecificInfo = () => {
|
||||||
if (dataSource.type === 'kv') {
|
if (dataSource.type === 'kv') {
|
||||||
return (
|
return (
|
||||||
@@ -27,18 +20,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataSource.type === 'computed') {
|
|
||||||
const depCount = getDependencyCount()
|
|
||||||
return (
|
|
||||||
<Flex align="center" gap="sm">
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
<ArrowsDownUp className="w-3 h-3 mr-1" />
|
|
||||||
{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +40,7 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
|||||||
{dependents.length > 0 && (
|
{dependents.length > 0 && (
|
||||||
<div className="pt-2 border-t border-border/50">
|
<div className="pt-2 border-t border-border/50">
|
||||||
<Text variant="caption">
|
<Text variant="caption">
|
||||||
Used by {dependents.length} computed {dependents.length === 1 ? 'source' : 'sources'}
|
Used by {dependents.length} dependent {dependents.length === 1 ? 'source' : 'sources'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
|||||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||||
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
||||||
import { ComputedSourceFields } from '@/components/molecules/data-source-editor/ComputedSourceFields'
|
|
||||||
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
||||||
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||||
|
|
||||||
interface DataSourceEditorDialogProps {
|
interface DataSourceEditorDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
dataSource: DataSource | null
|
dataSource: DataSource | null
|
||||||
allDataSources: DataSource[]
|
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onSave: (dataSource: DataSource) => void
|
onSave: (dataSource: DataSource) => void
|
||||||
}
|
}
|
||||||
@@ -20,19 +18,13 @@ interface DataSourceEditorDialogProps {
|
|||||||
export function DataSourceEditorDialog({
|
export function DataSourceEditorDialog({
|
||||||
open,
|
open,
|
||||||
dataSource,
|
dataSource,
|
||||||
allDataSources,
|
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSave,
|
onSave,
|
||||||
}: DataSourceEditorDialogProps) {
|
}: DataSourceEditorDialogProps) {
|
||||||
const {
|
const {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
} = useDataSourceEditor(dataSource)
|
||||||
removeDependency,
|
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
} = useDataSourceEditor(dataSource, allDataSources)
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!editingSource) return
|
if (!editingSource) return
|
||||||
@@ -80,18 +72,6 @@ export function DataSourceEditorDialog({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editingSource.type === 'computed' && (
|
|
||||||
<ComputedSourceFields
|
|
||||||
editingSource={editingSource}
|
|
||||||
availableDeps={availableDeps}
|
|
||||||
selectedDeps={selectedDeps}
|
|
||||||
unselectedDeps={unselectedDeps}
|
|
||||||
copy={dataSourceEditorCopy.computed}
|
|
||||||
onUpdateField={updateField}
|
|
||||||
onAddDependency={addDependency}
|
|
||||||
onRemoveDependency={removeDependency}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { DataSource } from '@/types/json-ui'
|
|
||||||
import { X } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface ComputedSourceFieldsCopy {
|
|
||||||
expressionLabel: string
|
|
||||||
expressionPlaceholder: string
|
|
||||||
expressionHelp: string
|
|
||||||
valueTemplateLabel: string
|
|
||||||
valueTemplatePlaceholder: string
|
|
||||||
valueTemplateHelp: string
|
|
||||||
dependenciesLabel: string
|
|
||||||
availableSourcesLabel: string
|
|
||||||
emptyDependencies: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComputedSourceFieldsProps {
|
|
||||||
editingSource: DataSource
|
|
||||||
availableDeps: DataSource[]
|
|
||||||
selectedDeps: string[]
|
|
||||||
unselectedDeps: DataSource[]
|
|
||||||
copy: ComputedSourceFieldsCopy
|
|
||||||
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
|
|
||||||
onAddDependency: (depId: string) => void
|
|
||||||
onRemoveDependency: (depId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ComputedSourceFields({
|
|
||||||
editingSource,
|
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
copy,
|
|
||||||
onUpdateField,
|
|
||||||
onAddDependency,
|
|
||||||
onRemoveDependency,
|
|
||||||
}: ComputedSourceFieldsProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>{copy.expressionLabel}</Label>
|
|
||||||
<Textarea
|
|
||||||
value={editingSource.expression || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
onUpdateField('expression', e.target.value)
|
|
||||||
}}
|
|
||||||
placeholder={copy.expressionPlaceholder}
|
|
||||||
className="font-mono text-sm h-24"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{copy.expressionHelp}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>{copy.valueTemplateLabel}</Label>
|
|
||||||
<Textarea
|
|
||||||
value={editingSource.valueTemplate ? JSON.stringify(editingSource.valueTemplate, null, 2) : ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
const template = JSON.parse(e.target.value)
|
|
||||||
onUpdateField('valueTemplate', template)
|
|
||||||
} catch (err) {
|
|
||||||
// Invalid JSON
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={copy.valueTemplatePlaceholder}
|
|
||||||
className="font-mono text-sm h-24"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{copy.valueTemplateHelp}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>{copy.dependenciesLabel}</Label>
|
|
||||||
|
|
||||||
{selectedDeps.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
|
|
||||||
{selectedDeps.map(depId => (
|
|
||||||
<Badge
|
|
||||||
key={depId}
|
|
||||||
variant="secondary"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{depId}
|
|
||||||
<button
|
|
||||||
onClick={() => onRemoveDependency(depId)}
|
|
||||||
className="ml-1 hover:text-destructive"
|
|
||||||
>
|
|
||||||
<X className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unselectedDeps.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs text-muted-foreground">{copy.availableSourcesLabel}</Label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{unselectedDeps.map(ds => (
|
|
||||||
<Button
|
|
||||||
key={ds.id}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onAddDependency(ds.id)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
>
|
|
||||||
+ {ds.id}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{availableDeps.length === 0 && selectedDeps.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{copy.emptyDependencies}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|||||||
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
||||||
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
||||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||||
import { Database, Function, FileText } from '@phosphor-icons/react'
|
import { Database, FileText } from '@phosphor-icons/react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { EmptyState, Stack } from '@/components/atoms'
|
import { EmptyState, Stack } from '@/components/atoms'
|
||||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||||
@@ -66,7 +66,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
|
|
||||||
const groupedSources = {
|
const groupedSources = {
|
||||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||||
computed: localSources.filter(ds => ds.type === 'computed'),
|
|
||||||
static: localSources.filter(ds => ds.type === 'static'),
|
static: localSources.filter(ds => ds.type === 'static'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,15 +109,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
onEdit={handleEditSource}
|
onEdit={handleEditSource}
|
||||||
onDelete={handleDeleteSource}
|
onDelete={handleDeleteSource}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataSourceGroupSection
|
|
||||||
icon={<Function size={16} />}
|
|
||||||
label={dataSourceManagerCopy.groups.computed}
|
|
||||||
dataSources={groupedSources.computed}
|
|
||||||
getDependents={getDependents}
|
|
||||||
onEdit={handleEditSource}
|
|
||||||
onDelete={handleDeleteSource}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -127,7 +117,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
<DataSourceEditorDialog
|
<DataSourceEditorDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
dataSource={editingSource}
|
dataSource={editingSource}
|
||||||
allDataSources={localSources}
|
|
||||||
onOpenChange={setDialogOpen}
|
onOpenChange={setDialogOpen}
|
||||||
onSave={handleSaveSource}
|
onSave={handleSaveSource}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||||
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
import { Plus, Database, FileText } from '@phosphor-icons/react'
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
import { DataSourceType } from '@/types/json-ui'
|
||||||
|
|
||||||
interface DataSourceManagerHeaderCopy {
|
interface DataSourceManagerHeaderCopy {
|
||||||
@@ -14,7 +14,6 @@ interface DataSourceManagerHeaderCopy {
|
|||||||
addLabel: string
|
addLabel: string
|
||||||
menu: {
|
menu: {
|
||||||
kv: string
|
kv: string
|
||||||
computed: string
|
|
||||||
static: string
|
static: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,10 +48,6 @@ export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeader
|
|||||||
<Database className="w-4 h-4 mr-2" />
|
<Database className="w-4 h-4 mr-2" />
|
||||||
{copy.menu.kv}
|
{copy.menu.kv}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onAdd('computed')}>
|
|
||||||
<Function className="w-4 h-4 mr-2" />
|
|
||||||
{copy.menu.computed}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => onAdd('static')}>
|
<DropdownMenuItem onClick={() => onAdd('static')}>
|
||||||
<FileText className="w-4 h-4 mr-2" />
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
{copy.menu.static}
|
{copy.menu.static}
|
||||||
|
|||||||
@@ -37,13 +37,6 @@ export function useDataSource(source: DataSource) {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
}
|
}
|
||||||
case 'computed':
|
|
||||||
return {
|
|
||||||
data: source.defaultValue,
|
|
||||||
setData: () => {},
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
@@ -67,7 +60,7 @@ export function useDataSources(sources: DataSource[]) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (source.type === 'static' || source.type === 'computed') {
|
if (source.type === 'static') {
|
||||||
updateData(source.id, source.defaultValue)
|
updateData(source.id, source.defaultValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const ActionSchema = z.object({
|
|||||||
|
|
||||||
export const DataSourceSchema = z.object({
|
export const DataSourceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.enum(['kv', 'api', 'computed', 'static'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'api', 'static'], { message: 'Invalid data source type' }),
|
||||||
key: z.string().optional(),
|
key: z.string().optional(),
|
||||||
endpoint: z.string().optional(),
|
endpoint: z.string().optional(),
|
||||||
transform: z.string().optional(),
|
transform: z.string().optional(),
|
||||||
|
|||||||
@@ -33,15 +33,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedTree",
|
"id": "selectedTree",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.trees?.find(t => t.id === data.selectedTreeId) || null",
|
"expression": "data.trees.find(id === data.selectedTreeId)",
|
||||||
"dependencies": ["trees", "selectedTreeId"]
|
"dependencies": [
|
||||||
|
"trees",
|
||||||
|
"selectedTreeId"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "treeCount",
|
"id": "treeCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.trees || []).length",
|
"expression": "data.trees.length",
|
||||||
"dependencies": ["trees"]
|
"dependencies": [
|
||||||
|
"trees"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -136,55 +141,145 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state",
|
"id": "tree-selection-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"conditional": {
|
||||||
"className": "flex-1 flex items-center justify-center"
|
"if": "selectedTree != null",
|
||||||
},
|
"then": {
|
||||||
"condition": {
|
"id": "tree-editor",
|
||||||
"source": "selectedTree",
|
|
||||||
"transform": "(val) => !val"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "empty-state-content",
|
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"className": "flex-1 p-6 overflow-auto"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state-title",
|
"id": "tree-header",
|
||||||
"type": "Heading",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-2xl font-bold text-muted-foreground",
|
"className": "mb-6"
|
||||||
"children": "No Tree Selected"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-description",
|
|
||||||
"type": "Text",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground",
|
|
||||||
"children": "Select a component tree from the sidebar or create a new one"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-button",
|
|
||||||
"type": "Button",
|
|
||||||
"props": {
|
|
||||||
"variant": "default",
|
|
||||||
"children": "Create Your First Tree"
|
|
||||||
},
|
},
|
||||||
"events": [
|
"children": [
|
||||||
{
|
{
|
||||||
"event": "click",
|
"id": "tree-name",
|
||||||
"actions": [
|
"type": "Heading",
|
||||||
|
"props": {
|
||||||
|
"className": "text-3xl font-bold mb-2"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedTree",
|
||||||
|
"path": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tree-description",
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedTree",
|
||||||
|
"path": "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tree-canvas",
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"className": "min-h-[500px]"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "canvas-header",
|
||||||
|
"type": "CardHeader",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "open-create-from-empty",
|
"id": "canvas-title",
|
||||||
"type": "set-value",
|
"type": "CardTitle",
|
||||||
"target": "createDialogOpen",
|
"props": {
|
||||||
"value": true
|
"children": "Component Hierarchy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "canvas-description",
|
||||||
|
"type": "CardDescription",
|
||||||
|
"props": {
|
||||||
|
"children": "Build your component tree structure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "canvas-content",
|
||||||
|
"type": "CardContent",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "canvas-placeholder",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center text-muted-foreground py-12 border-2 border-dashed border-border rounded-lg",
|
||||||
|
"children": "Component tree builder - Add components to build your hierarchy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"id": "empty-state",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex-1 flex items-center justify-center"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center space-y-4"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-title",
|
||||||
|
"type": "Heading",
|
||||||
|
"props": {
|
||||||
|
"className": "text-2xl font-bold text-muted-foreground",
|
||||||
|
"children": "No Tree Selected"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-description",
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground",
|
||||||
|
"children": "Select a component tree from the sidebar or create a new one"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-button",
|
||||||
|
"type": "Button",
|
||||||
|
"props": {
|
||||||
|
"variant": "default",
|
||||||
|
"children": "Create Your First Tree"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"event": "click",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "open-create-from-empty",
|
||||||
|
"type": "set-value",
|
||||||
|
"target": "createDialogOpen",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -192,98 +287,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tree-editor",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"source": "selectedTree",
|
|
||||||
"transform": "(val) => !!val"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "tree-header",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "mb-6"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "tree-name",
|
|
||||||
"type": "Heading",
|
|
||||||
"props": {
|
|
||||||
"className": "text-3xl font-bold mb-2"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedTree",
|
|
||||||
"path": "name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tree-description",
|
|
||||||
"type": "Text",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedTree",
|
|
||||||
"path": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tree-canvas",
|
|
||||||
"type": "Card",
|
|
||||||
"props": {
|
|
||||||
"className": "min-h-[500px]"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "canvas-header",
|
|
||||||
"type": "CardHeader",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "canvas-title",
|
|
||||||
"type": "CardTitle",
|
|
||||||
"props": {
|
|
||||||
"children": "Component Hierarchy"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "canvas-description",
|
|
||||||
"type": "CardDescription",
|
|
||||||
"props": {
|
|
||||||
"children": "Build your component tree structure"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "canvas-content",
|
|
||||||
"type": "CardContent",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "canvas-placeholder",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "text-center text-muted-foreground py-12 border-2 border-dashed border-border rounded-lg",
|
|
||||||
"children": "Component tree builder - Add components to build your hierarchy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -291,4 +295,4 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalActions": []
|
"globalActions": []
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,6 @@
|
|||||||
"title": "Project Completeness",
|
"title": "Project Completeness",
|
||||||
"icon": "CheckCircle",
|
"icon": "CheckCircle",
|
||||||
"gradient": "from-primary/10 to-accent/10",
|
"gradient": "from-primary/10 to-accent/10",
|
||||||
"dataSource": {
|
|
||||||
"type": "computed",
|
|
||||||
"compute": "calculateCompletionScore"
|
|
||||||
},
|
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"type": "metric",
|
"type": "metric",
|
||||||
|
|||||||
@@ -133,9 +133,11 @@
|
|||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": "activeFile",
|
"id": "activeFile",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"dependencies": ["files", "activeFileId"],
|
"expression": "data.files.0",
|
||||||
"compute": "context.files.find(f => f.id === context.activeFileId)"
|
"dependencies": [
|
||||||
|
"files"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
|
|||||||
@@ -35,27 +35,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedBlueprint",
|
"id": "selectedBlueprint",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.find(b => b.id === data.selectedBlueprintId) || null; }",
|
"expression": "data.flaskConfig.blueprints.find(id === data.selectedBlueprintId)",
|
||||||
"dependencies": ["flaskConfig", "selectedBlueprintId"]
|
"dependencies": [
|
||||||
|
"flaskConfig",
|
||||||
|
"selectedBlueprintId"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "blueprintCount",
|
"id": "blueprintCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => ((data.flaskConfig || {}).blueprints || []).length",
|
"expression": "data.flaskConfig.blueprints.length",
|
||||||
"dependencies": ["flaskConfig"]
|
"dependencies": [
|
||||||
|
"flaskConfig"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "endpointCount",
|
"id": "endpointCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const bp = data.selectedBlueprint; return bp ? (bp.endpoints || []).length : 0; }",
|
"expression": "data.selectedBlueprint.endpoints.length",
|
||||||
"dependencies": ["selectedBlueprint"]
|
"dependencies": [
|
||||||
},
|
"selectedBlueprint"
|
||||||
{
|
]
|
||||||
"id": "totalEndpoints",
|
|
||||||
"type": "computed",
|
|
||||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.reduce((sum, bp) => sum + (bp.endpoints || []).length, 0); }",
|
|
||||||
"dependencies": ["flaskConfig"]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedLambda",
|
"id": "selectedLambda",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.lambdas?.find(l => l.id === data.selectedLambdaId) || null",
|
"expression": "data.lambdas.find(id === data.selectedLambdaId)",
|
||||||
"dependencies": ["lambdas", "selectedLambdaId"]
|
"dependencies": [
|
||||||
|
"lambdas",
|
||||||
|
"selectedLambdaId"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lambdaCount",
|
"id": "lambdaCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.lambdas || []).length",
|
"expression": "data.lambdas.length",
|
||||||
"dependencies": ["lambdas"]
|
"dependencies": [
|
||||||
|
"lambdas"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -71,7 +76,9 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent"
|
"className": "text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent"
|
||||||
},
|
},
|
||||||
"children": ["Lambdas"]
|
"children": [
|
||||||
|
"Lambdas"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Badge",
|
"type": "Badge",
|
||||||
@@ -133,7 +140,9 @@
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"children": ["Lambda list will be rendered here"]
|
"children": [
|
||||||
|
"Lambda list will be rendered here"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -159,14 +168,18 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-lg font-semibold mb-2"
|
"className": "text-lg font-semibold mb-2"
|
||||||
},
|
},
|
||||||
"children": ["No Lambdas Yet"]
|
"children": [
|
||||||
|
"No Lambdas Yet"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "p",
|
"type": "p",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-sm text-muted-foreground mb-4"
|
"className": "text-sm text-muted-foreground mb-4"
|
||||||
},
|
},
|
||||||
"children": ["Create your first serverless function"]
|
"children": [
|
||||||
|
"Create your first serverless function"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -182,101 +195,106 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
"id": "lambda-selection-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
|
||||||
"className": "flex-1 flex items-center justify-center p-8"
|
|
||||||
},
|
|
||||||
"conditional": {
|
"conditional": {
|
||||||
"if": "selectedLambda"
|
"if": "selectedLambda != null",
|
||||||
},
|
"then": {
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "max-w-6xl mx-auto w-full space-y-6"
|
"className": "flex-1 flex items-center justify-center p-8"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex items-center justify-between"
|
"className": "max-w-6xl mx-auto w-full space-y-6"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex items-center justify-between"
|
||||||
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "h1",
|
"type": "div",
|
||||||
"props": {
|
"children": [
|
||||||
"className": "text-3xl font-bold"
|
{
|
||||||
},
|
"type": "h1",
|
||||||
"bindings": {
|
"props": {
|
||||||
"children": {
|
"className": "text-3xl font-bold"
|
||||||
"source": "selectedLambda",
|
},
|
||||||
"path": "name"
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedLambda",
|
||||||
|
"path": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedLambda",
|
||||||
|
"path": "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "p",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedLambda",
|
|
||||||
"path": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
]
|
"else": {
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 flex items-center justify-center p-8"
|
|
||||||
},
|
|
||||||
"conditional": {
|
|
||||||
"if": "!selectedLambda"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center"
|
"className": "flex-1 flex items-center justify-center p-8"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "icon",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"name": "Code",
|
"className": "text-center"
|
||||||
"className": "h-20 w-20 text-muted-foreground/50 mx-auto mb-4",
|
|
||||||
"weight": "duotone"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "h3",
|
|
||||||
"props": {
|
|
||||||
"className": "text-xl font-semibold mb-2"
|
|
||||||
},
|
},
|
||||||
"children": ["No Lambda Selected"]
|
"children": [
|
||||||
},
|
{
|
||||||
{
|
"type": "icon",
|
||||||
"type": "p",
|
"props": {
|
||||||
"props": {
|
"name": "Code",
|
||||||
"className": "text-muted-foreground"
|
"className": "h-20 w-20 text-muted-foreground/50 mx-auto mb-4",
|
||||||
},
|
"weight": "duotone"
|
||||||
"children": ["Select a lambda from the sidebar or create a new one"]
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "h3",
|
||||||
|
"props": {
|
||||||
|
"className": "text-xl font-semibold mb-2"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
"No Lambda Selected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
"Select a lambda from the sidebar or create a new one"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedModel",
|
"id": "selectedModel",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.models?.find(m => m.id === data.selectedModelId) || null",
|
"expression": "data.models.find(id === data.selectedModelId)",
|
||||||
"dependencies": ["models", "selectedModelId"]
|
"dependencies": [
|
||||||
|
"models",
|
||||||
|
"selectedModelId"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "modelCount",
|
"id": "modelCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.models || []).length",
|
"expression": "data.models.length",
|
||||||
"dependencies": ["models"]
|
"dependencies": [
|
||||||
|
"models"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -131,55 +136,142 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state",
|
"id": "model-selection-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"conditional": {
|
||||||
"className": "flex-1 flex items-center justify-center"
|
"if": "selectedModel != null",
|
||||||
},
|
"then": {
|
||||||
"condition": {
|
"id": "model-editor",
|
||||||
"source": "selectedModel",
|
|
||||||
"transform": "(val) => !val"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "empty-state-content",
|
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"className": "flex-1 p-6 overflow-auto"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state-title",
|
"id": "model-header",
|
||||||
"type": "Heading",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-2xl font-bold text-muted-foreground",
|
"className": "mb-6"
|
||||||
"children": "No Model Selected"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-description",
|
|
||||||
"type": "Text",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground",
|
|
||||||
"children": "Select a model from the sidebar or create a new one"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-button",
|
|
||||||
"type": "Button",
|
|
||||||
"props": {
|
|
||||||
"variant": "default",
|
|
||||||
"children": "Create Your First Model"
|
|
||||||
},
|
},
|
||||||
"events": [
|
"children": [
|
||||||
{
|
{
|
||||||
"event": "click",
|
"id": "model-name",
|
||||||
"actions": [
|
"type": "Heading",
|
||||||
|
"props": {
|
||||||
|
"className": "text-3xl font-bold mb-2"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedModel",
|
||||||
|
"path": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model-description",
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedModel",
|
||||||
|
"path": "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fields-card",
|
||||||
|
"type": "Card",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fields-header",
|
||||||
|
"type": "CardHeader",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "open-create-from-empty",
|
"id": "fields-title",
|
||||||
"type": "set-value",
|
"type": "CardTitle",
|
||||||
"target": "createDialogOpen",
|
"props": {
|
||||||
"value": true
|
"children": "Model Fields"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fields-description",
|
||||||
|
"type": "CardDescription",
|
||||||
|
"props": {
|
||||||
|
"children": "Define the fields and their types for this model"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fields-content",
|
||||||
|
"type": "CardContent",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "fields-placeholder",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center text-muted-foreground py-8 border-2 border-dashed border-border rounded-lg",
|
||||||
|
"children": "Add fields to define your data model"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"id": "empty-state",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex-1 flex items-center justify-center"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center space-y-4"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-title",
|
||||||
|
"type": "Heading",
|
||||||
|
"props": {
|
||||||
|
"className": "text-2xl font-bold text-muted-foreground",
|
||||||
|
"children": "No Model Selected"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-description",
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground",
|
||||||
|
"children": "Select a model from the sidebar or create a new one"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-button",
|
||||||
|
"type": "Button",
|
||||||
|
"props": {
|
||||||
|
"variant": "default",
|
||||||
|
"children": "Create Your First Model"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"event": "click",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "open-create-from-empty",
|
||||||
|
"type": "set-value",
|
||||||
|
"target": "createDialogOpen",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -187,95 +279,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "model-editor",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"source": "selectedModel",
|
|
||||||
"transform": "(val) => !!val"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "model-header",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "mb-6"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "model-name",
|
|
||||||
"type": "Heading",
|
|
||||||
"props": {
|
|
||||||
"className": "text-3xl font-bold mb-2"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedModel",
|
|
||||||
"path": "name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "model-description",
|
|
||||||
"type": "Text",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedModel",
|
|
||||||
"path": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fields-card",
|
|
||||||
"type": "Card",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "fields-header",
|
|
||||||
"type": "CardHeader",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "fields-title",
|
|
||||||
"type": "CardTitle",
|
|
||||||
"props": {
|
|
||||||
"children": "Model Fields"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fields-description",
|
|
||||||
"type": "CardDescription",
|
|
||||||
"props": {
|
|
||||||
"children": "Define the fields and their types for this model"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fields-content",
|
|
||||||
"type": "CardContent",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "fields-placeholder",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "text-center text-muted-foreground py-8 border-2 border-dashed border-border rounded-lg",
|
|
||||||
"children": "Add fields to define your data model"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,63 +5,15 @@
|
|||||||
"id": "lastSaved",
|
"id": "lastSaved",
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "currentTime",
|
|
||||||
"type": "static",
|
|
||||||
"defaultValue": 0,
|
|
||||||
"polling": {
|
|
||||||
"interval": 10000,
|
|
||||||
"update": "() => Date.now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "isRecent",
|
|
||||||
"type": "computed",
|
|
||||||
"compute": "(data) => { if (!data.lastSaved) return false; return Date.now() - data.lastSaved < 3000; }",
|
|
||||||
"dependencies": ["lastSaved", "currentTime"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "timeAgo",
|
|
||||||
"type": "computed",
|
|
||||||
"compute": "(data) => { if (!data.lastSaved) return ''; const seconds = Math.floor((Date.now() - data.lastSaved) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; return `${Math.floor(seconds / 86400)}d ago`; }",
|
|
||||||
"dependencies": ["lastSaved", "currentTime"]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "div",
|
"type": "SaveIndicator",
|
||||||
"props": {
|
|
||||||
"className": "flex items-center gap-1.5 text-xs text-muted-foreground"
|
|
||||||
},
|
|
||||||
"conditional": {
|
"conditional": {
|
||||||
"if": "lastSaved !== null"
|
"if": "lastSaved != null"
|
||||||
},
|
},
|
||||||
"children": [
|
"bindings": {
|
||||||
{
|
"lastSaved": {
|
||||||
"id": "status-icon",
|
"source": "lastSaved"
|
||||||
"type": "StatusIcon",
|
|
||||||
"dataBinding": {
|
|
||||||
"type": {
|
|
||||||
"source": "isRecent",
|
|
||||||
"transform": "isRecent => isRecent ? 'saved' : 'synced'"
|
|
||||||
},
|
|
||||||
"animate": {
|
|
||||||
"source": "isRecent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "time-text",
|
|
||||||
"type": "span",
|
|
||||||
"props": {
|
|
||||||
"className": "hidden sm:inline"
|
|
||||||
},
|
|
||||||
"dataBinding": {
|
|
||||||
"children": {
|
|
||||||
"source": "isRecent",
|
|
||||||
"path": null,
|
|
||||||
"transform": "(isRecent, data) => isRecent ? 'Saved' : data.timeAgo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,21 +54,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "activeVariant",
|
"id": "activeVariant",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const theme = data.theme || {}; const variants = theme.variants || []; return variants.find(v => v.id === theme.activeVariantId) || variants[0] || null; }",
|
"expression": "data.theme.variants.find(id === data.theme.activeVariantId)",
|
||||||
"dependencies": ["theme"]
|
"dependencies": [
|
||||||
|
"theme"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "variantCount",
|
"id": "variantCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => ((data.theme || {}).variants || []).length",
|
"expression": "data.theme.variants.length",
|
||||||
"dependencies": ["theme"]
|
"dependencies": [
|
||||||
|
"theme"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "customColorCount",
|
"id": "customColorCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const variant = data.activeVariant; if (!variant || !variant.colors) return 0; return Object.keys(variant.colors.customColors || {}).length; }",
|
"expression": "Object.keys(data.activeVariant.colors.customColors).length",
|
||||||
"dependencies": ["activeVariant"]
|
"dependencies": [
|
||||||
|
"activeVariant"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -32,15 +32,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedWorkflow",
|
"id": "selectedWorkflow",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.workflows?.find(w => w.id === data.selectedWorkflowId) || null",
|
"expression": "data.workflows.find(id === data.selectedWorkflowId)",
|
||||||
"dependencies": ["workflows", "selectedWorkflowId"]
|
"dependencies": [
|
||||||
|
"workflows",
|
||||||
|
"selectedWorkflowId"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "workflowCount",
|
"id": "workflowCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.workflows || []).length",
|
"expression": "data.workflows.length",
|
||||||
"dependencies": ["workflows"]
|
"dependencies": [
|
||||||
|
"workflows"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -71,7 +76,9 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-xl font-bold mb-2 flex items-center gap-2"
|
"className": "text-xl font-bold mb-2 flex items-center gap-2"
|
||||||
},
|
},
|
||||||
"children": ["Workflows"]
|
"children": [
|
||||||
|
"Workflows"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "create-button",
|
"id": "create-button",
|
||||||
@@ -117,7 +124,9 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-sm text-muted-foreground"
|
"className": "text-sm text-muted-foreground"
|
||||||
},
|
},
|
||||||
"children": ["Status Filter"]
|
"children": [
|
||||||
|
"Status Filter"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -136,7 +145,9 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-center py-8 text-muted-foreground"
|
"className": "text-center py-8 text-muted-foreground"
|
||||||
},
|
},
|
||||||
"children": ["No workflows yet"]
|
"children": [
|
||||||
|
"No workflows yet"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -150,122 +161,129 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state",
|
"id": "workflow-selection-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
|
||||||
"className": "flex-1 flex items-center justify-center"
|
|
||||||
},
|
|
||||||
"conditional": {
|
"conditional": {
|
||||||
"if": "!selectedWorkflow"
|
"if": "selectedWorkflow != null",
|
||||||
},
|
"then": {
|
||||||
"children": [
|
"id": "workflow-editor",
|
||||||
{
|
|
||||||
"id": "empty-state-content",
|
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"className": "flex-1 p-6 overflow-auto"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "icon",
|
"id": "workflow-header",
|
||||||
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"name": "GitBranch",
|
"className": "mb-6"
|
||||||
"className": "h-20 w-20 text-muted-foreground/50 mx-auto",
|
|
||||||
"weight": "duotone"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-title",
|
|
||||||
"type": "h3",
|
|
||||||
"props": {
|
|
||||||
"className": "text-2xl font-bold text-muted-foreground"
|
|
||||||
},
|
|
||||||
"children": ["No Workflow Selected"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-state-description",
|
|
||||||
"type": "p",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground"
|
|
||||||
},
|
|
||||||
"children": ["Select a workflow from the sidebar or create a new one"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "workflow-editor",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
|
||||||
},
|
|
||||||
"conditional": {
|
|
||||||
"if": "selectedWorkflow"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "workflow-header",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "mb-6"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "workflow-name",
|
|
||||||
"type": "h1",
|
|
||||||
"props": {
|
|
||||||
"className": "text-3xl font-bold mb-2"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedWorkflow",
|
|
||||||
"path": "name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "workflow-description",
|
|
||||||
"type": "p",
|
|
||||||
"props": {
|
|
||||||
"className": "text-muted-foreground"
|
|
||||||
},
|
|
||||||
"bindings": {
|
|
||||||
"children": {
|
|
||||||
"source": "selectedWorkflow",
|
|
||||||
"path": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "workflow-canvas",
|
|
||||||
"type": "Card",
|
|
||||||
"props": {
|
|
||||||
"className": "min-h-[400px] bg-muted/20"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "canvas-content",
|
|
||||||
"type": "CardContent",
|
|
||||||
"props": {
|
|
||||||
"className": "p-6"
|
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "canvas-placeholder",
|
"id": "workflow-name",
|
||||||
"type": "div",
|
"type": "h1",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center text-muted-foreground py-12"
|
"className": "text-3xl font-bold mb-2"
|
||||||
},
|
},
|
||||||
"children": ["Workflow canvas - Add nodes to build your workflow"]
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedWorkflow",
|
||||||
|
"path": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workflow-description",
|
||||||
|
"type": "p",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"children": {
|
||||||
|
"source": "selectedWorkflow",
|
||||||
|
"path": "description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workflow-canvas",
|
||||||
|
"type": "Card",
|
||||||
|
"props": {
|
||||||
|
"className": "min-h-[400px] bg-muted/20"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "canvas-content",
|
||||||
|
"type": "CardContent",
|
||||||
|
"props": {
|
||||||
|
"className": "p-6"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "canvas-placeholder",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center text-muted-foreground py-12"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
"Workflow canvas - Add nodes to build your workflow"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"id": "empty-state",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex-1 flex items-center justify-center"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-center space-y-4"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon",
|
||||||
|
"props": {
|
||||||
|
"name": "GitBranch",
|
||||||
|
"className": "h-20 w-20 text-muted-foreground/50 mx-auto",
|
||||||
|
"weight": "duotone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-title",
|
||||||
|
"type": "h3",
|
||||||
|
"props": {
|
||||||
|
"className": "text-2xl font-bold text-muted-foreground"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
"No Workflow Selected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "empty-state-description",
|
||||||
|
"type": "p",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
"Select a workflow from the sidebar or create a new one"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,36 +15,36 @@
|
|||||||
"key": "dashboard",
|
"key": "dashboard",
|
||||||
"name": "Dashboard",
|
"name": "Dashboard",
|
||||||
"description": "Complete dashboard with stats, activity feed, and quick actions",
|
"description": "Complete dashboard with stats, activity feed, and quick actions",
|
||||||
"icon": "ChartBar",
|
"iconId": "ChartBar",
|
||||||
"configKey": "dashboard"
|
"configPath": "/src/config/ui-examples/dashboard.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "form",
|
"key": "form",
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"description": "Dynamic form with validation and data binding",
|
"description": "Dynamic form with validation and data binding",
|
||||||
"icon": "ListBullets",
|
"iconId": "ListBullets",
|
||||||
"configKey": "form"
|
"configPath": "/src/config/ui-examples/form.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "table",
|
"key": "table",
|
||||||
"name": "Data Table",
|
"name": "Data Table",
|
||||||
"description": "Interactive table with row actions and looping",
|
"description": "Interactive table with row actions and looping",
|
||||||
"icon": "Table",
|
"iconId": "Table",
|
||||||
"configKey": "table"
|
"configPath": "/src/config/ui-examples/table.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "bindings",
|
"key": "bindings",
|
||||||
"name": "Bindings",
|
"name": "Bindings",
|
||||||
"description": "List, table, and timeline bindings with shared data sources",
|
"description": "List, table, and timeline bindings with shared data sources",
|
||||||
"icon": "Clock",
|
"iconId": "Clock",
|
||||||
"configKey": "list-table-timeline"
|
"configPath": "/src/config/ui-examples/list-table-timeline.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "settings",
|
"key": "settings",
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"description": "Tabbed settings panel with switches and selections",
|
"description": "Tabbed settings panel with switches and selections",
|
||||||
"icon": "Gear",
|
"iconId": "Gear",
|
||||||
"configKey": "settings"
|
"configPath": "/src/config/ui-examples/settings.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Binding Designer",
|
"title": "Data Binding Designer",
|
||||||
"description": "Connect UI components to KV storage and computed values"
|
"description": "Connect UI components to KV storage and static data"
|
||||||
},
|
},
|
||||||
"bindingsCard": {
|
"bindingsCard": {
|
||||||
"title": "Component Bindings",
|
"title": "Component Bindings",
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
"title": "How It Works",
|
"title": "How It Works",
|
||||||
"steps": [
|
"steps": [
|
||||||
"Create data sources (KV store for persistence, static for constants)",
|
"Create data sources (KV store for persistence, static for constants)",
|
||||||
"Add computed sources to derive values from other sources",
|
|
||||||
"Bind component properties to data sources for reactive updates"
|
"Bind component properties to data sources for reactive updates"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -34,12 +33,6 @@
|
|||||||
"key": "app-counter",
|
"key": "app-counter",
|
||||||
"defaultValue": 0
|
"defaultValue": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "displayName",
|
|
||||||
"type": "computed",
|
|
||||||
"dependencies": ["userProfile"],
|
|
||||||
"expression": "data.userProfile.name"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -50,7 +43,8 @@
|
|||||||
},
|
},
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"children": {
|
"children": {
|
||||||
"source": "displayName"
|
"source": "userProfile",
|
||||||
|
"path": "name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Edit Data Source",
|
"title": "Edit Data Source",
|
||||||
"description": "Configure the data source settings and dependencies",
|
"description": "Configure the data source settings",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": {
|
"id": {
|
||||||
"label": "ID",
|
"label": "ID",
|
||||||
@@ -18,17 +18,6 @@
|
|||||||
"valueLabel": "Value (JSON)",
|
"valueLabel": "Value (JSON)",
|
||||||
"valuePlaceholder": "{\"key\": \"value\"}"
|
"valuePlaceholder": "{\"key\": \"value\"}"
|
||||||
},
|
},
|
||||||
"computed": {
|
|
||||||
"expressionLabel": "Expression",
|
|
||||||
"expressionPlaceholder": "data.source1",
|
|
||||||
"expressionHelp": "Expression that computes the value from other data sources",
|
|
||||||
"valueTemplateLabel": "Value Template (JSON)",
|
|
||||||
"valueTemplatePlaceholder": "{\n \"total\": \"data.items.length\"\n}",
|
|
||||||
"valueTemplateHelp": "Template object with expressions for computed fields",
|
|
||||||
"dependenciesLabel": "Dependencies",
|
|
||||||
"availableSourcesLabel": "Available Sources",
|
|
||||||
"emptyDependencies": "No data sources available. Create KV or static sources first."
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save Changes"
|
"save": "Save Changes"
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Sources",
|
"title": "Data Sources",
|
||||||
"description": "Manage KV storage, computed values, and static data"
|
"description": "Manage KV storage and static data"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"add": "Add Data Source"
|
"add": "Add Data Source"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"kv": "KV Store",
|
"kv": "KV Store",
|
||||||
"computed": "Computed Value",
|
|
||||||
"static": "Static Data"
|
"static": "Static Data"
|
||||||
},
|
},
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
@@ -17,12 +16,11 @@
|
|||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"kv": "KV Store",
|
"kv": "KV Store",
|
||||||
"static": "Static Data",
|
"static": "Static Data"
|
||||||
"computed": "Computed Values"
|
|
||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"deleteBlockedTitle": "Cannot delete",
|
"deleteBlockedTitle": "Cannot delete",
|
||||||
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
|
"deleteBlockedDescription": "This source is used by {count} dependent {noun}",
|
||||||
"deleted": "Data source deleted",
|
"deleted": "Data source deleted",
|
||||||
"updated": "Data source updated"
|
"updated": "Data source updated"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
|
||||||
export function useDataSourceEditor(
|
export function useDataSourceEditor(
|
||||||
dataSource: DataSource | null,
|
dataSource: DataSource | null,
|
||||||
allDataSources: DataSource[],
|
|
||||||
) {
|
) {
|
||||||
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
||||||
|
|
||||||
@@ -15,44 +14,8 @@ export function useDataSourceEditor(
|
|||||||
setEditingSource(prev => (prev ? { ...prev, [field]: value } : prev))
|
setEditingSource(prev => (prev ? { ...prev, [field]: value } : prev))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource(prev => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
if (deps.includes(depId)) return prev
|
|
||||||
return { ...prev, dependencies: [...deps, depId] }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource(prev => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
return { ...prev, dependencies: deps.filter(dep => dep !== depId) }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const availableDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return allDataSources.filter(
|
|
||||||
ds => ds.id !== editingSource.id && ds.type !== 'computed',
|
|
||||||
)
|
|
||||||
}, [allDataSources, editingSource])
|
|
||||||
|
|
||||||
const selectedDeps = useMemo(() => editingSource?.dependencies || [], [editingSource])
|
|
||||||
|
|
||||||
const unselectedDeps = useMemo(
|
|
||||||
() => availableDeps.filter(ds => !selectedDeps.includes(ds.id)),
|
|
||||||
[availableDeps, selectedDeps],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
|
||||||
removeDependency,
|
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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' && { expression: '', dependencies: [] }),
|
|
||||||
...(type === 'static' && { defaultValue: null }),
|
...(type === 'static' && { defaultValue: null }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
|||||||
|
|
||||||
const getDependents = useCallback((sourceId: string) => {
|
const getDependents = useCallback((sourceId: string) => {
|
||||||
return dataSources.filter(ds =>
|
return dataSources.filter(ds =>
|
||||||
ds.type === 'computed' &&
|
|
||||||
ds.dependencies?.includes(sourceId)
|
ds.dependencies?.includes(sourceId)
|
||||||
)
|
)
|
||||||
}, [dataSources])
|
}, [dataSources])
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { useKV } from '@/hooks/use-kv'
|
import { useKV } from '@/hooks/use-kv'
|
||||||
|
|
||||||
export type DataSourceType = 'kv' | 'static' | 'computed'
|
export type DataSourceType = 'kv' | 'static'
|
||||||
|
|
||||||
export interface DataSourceConfig<T = any> {
|
export interface DataSourceConfig<T = any> {
|
||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
key?: string
|
key?: string
|
||||||
defaultValue?: T
|
defaultValue?: T
|
||||||
compute?: (allData: Record<string, any>) => T
|
|
||||||
dependencies?: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useKVDataSource<T = any>(key: string, defaultValue?: T) {
|
export function useKVDataSource<T = any>(key: string, defaultValue?: T) {
|
||||||
@@ -18,13 +16,6 @@ export function useStaticDataSource<T = any>(defaultValue: T) {
|
|||||||
return [defaultValue, () => {}, () => {}] as const
|
return [defaultValue, () => {}, () => {}] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useComputedDataSource<T = any>(
|
|
||||||
compute: (allData: Record<string, any>) => T,
|
|
||||||
dependencies: Record<string, any>
|
|
||||||
) {
|
|
||||||
return compute(dependencies)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMultipleDataSources(_sources: DataSourceConfig[]) {
|
export function useMultipleDataSources(_sources: DataSourceConfig[]) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,20 +41,20 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
const derivedSources = dataSources.filter(ds => ds.expression || ds.valueTemplate)
|
||||||
|
|
||||||
computedSources.forEach(source => {
|
derivedSources.forEach(source => {
|
||||||
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 evaluationContext = { data }
|
const evaluationContext = { data }
|
||||||
const computedValue = source.expression
|
const derivedValue = source.expression
|
||||||
? evaluateExpression(source.expression, evaluationContext)
|
? evaluateExpression(source.expression, evaluationContext)
|
||||||
: source.valueTemplate
|
: source.valueTemplate
|
||||||
? evaluateTemplate(source.valueTemplate, evaluationContext)
|
? evaluateTemplate(source.valueTemplate, evaluationContext)
|
||||||
: source.defaultValue
|
: source.defaultValue
|
||||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
setData(prev => ({ ...prev, [source.id]: derivedValue }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [data, dataSources])
|
}, [data, dataSources])
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
[dataSources]
|
[dataSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
const computedSources = useMemo(
|
const derivedSources = useMemo(
|
||||||
() => dataSources.filter((ds) => ds.type === 'computed'),
|
() => dataSources.filter((ds) => ds.expression || ds.valueTemplate),
|
||||||
[dataSources]
|
[dataSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
const computedData = useMemo(() => {
|
const computedData = useMemo(() => {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
|
||||||
computedSources.forEach((ds) => {
|
derivedSources.forEach((ds) => {
|
||||||
const evaluationContext = { data }
|
const evaluationContext = { data: { ...data, ...result } }
|
||||||
if (ds.expression) {
|
if (ds.expression) {
|
||||||
result[ds.id] = evaluateExpression(ds.expression, evaluationContext)
|
result[ds.id] = evaluateExpression(ds.expression, evaluationContext)
|
||||||
return
|
return
|
||||||
@@ -70,7 +70,7 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}, [computedSources, data])
|
}, [derivedSources, data])
|
||||||
|
|
||||||
const allData = useMemo(
|
const allData = useMemo(
|
||||||
() => ({ ...data, ...computedData }),
|
() => ({ ...data, ...computedData }),
|
||||||
|
|||||||
@@ -45,22 +45,26 @@ export function usePage(schema: PageSchema) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema.data) {
|
if (schema.data) {
|
||||||
const computed: Record<string, any> = {}
|
const computed: Record<string, any> = {}
|
||||||
|
|
||||||
schema.data.forEach(source => {
|
schema.data.forEach(source => {
|
||||||
if (source.type === 'computed') {
|
if (source.expression) {
|
||||||
if (source.expression) {
|
computed[source.id] = evaluateBindingExpression(source.expression, { ...dataContext, ...computed }, {
|
||||||
computed[source.id] = evaluateBindingExpression(source.expression, dataContext, {
|
fallback: undefined,
|
||||||
fallback: undefined,
|
label: `derived data (${source.id})`,
|
||||||
label: `computed data (${source.id})`,
|
})
|
||||||
})
|
return
|
||||||
} else if (source.valueTemplate) {
|
}
|
||||||
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: dataContext })
|
|
||||||
}
|
if (source.valueTemplate) {
|
||||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: { ...dataContext, ...computed } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||||
computed[source.id] = source.defaultValue
|
computed[source.id] = source.defaultValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setComputedData(computed)
|
setComputedData(computed)
|
||||||
}
|
}
|
||||||
}, [schema.data, dataContext])
|
}, [schema.data, dataContext])
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
|
||||||
interface UseDataSourceEditorParams {
|
interface UseDataSourceEditorParams {
|
||||||
dataSource: DataSource | null
|
dataSource: DataSource | null
|
||||||
allDataSources: DataSource[]
|
|
||||||
onSave: (dataSource: DataSource) => void
|
onSave: (dataSource: DataSource) => void
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDataSourceEditor({
|
export function useDataSourceEditor({
|
||||||
dataSource,
|
dataSource,
|
||||||
allDataSources,
|
|
||||||
onSave,
|
onSave,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: UseDataSourceEditorParams) {
|
}: UseDataSourceEditorParams) {
|
||||||
@@ -27,51 +25,15 @@ export function useDataSourceEditor({
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource((prev) => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
if (deps.includes(depId)) return prev
|
|
||||||
return { ...prev, dependencies: [...deps, depId] }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource((prev) => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
return { ...prev, dependencies: deps.filter((id) => id !== depId) }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
if (!editingSource) return
|
if (!editingSource) return
|
||||||
onSave(editingSource)
|
onSave(editingSource)
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
}, [editingSource, onOpenChange, onSave])
|
}, [editingSource, onOpenChange, onSave])
|
||||||
|
|
||||||
const availableDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return allDataSources.filter(
|
|
||||||
(ds) => ds.id !== editingSource.id && ds.type !== 'computed',
|
|
||||||
)
|
|
||||||
}, [allDataSources, editingSource])
|
|
||||||
|
|
||||||
const selectedDeps = useMemo(() => editingSource?.dependencies || [], [editingSource])
|
|
||||||
|
|
||||||
const unselectedDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return availableDeps.filter((ds) => !selectedDeps.includes(ds.id))
|
|
||||||
}, [availableDeps, editingSource, selectedDeps])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
|
||||||
removeDependency,
|
|
||||||
handleSave,
|
handleSave,
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,101 @@
|
|||||||
|
|
||||||
import { PrismaModel, ComponentNode, ThemeConfig, ProjectFile } from '@/types/project'
|
import { PrismaModel, ComponentNode, ThemeConfig, ProjectFile } from '@/types/project'
|
||||||
import { ProtectedLLMService } from './protected-llm-service'
|
import { ProtectedLLMService } from './protected-llm-service'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const componentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() => z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
props: z.record(z.any()),
|
||||||
|
children: z.array(componentNodeSchema)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const prismaFieldSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
isRequired: z.boolean(),
|
||||||
|
isUnique: z.boolean(),
|
||||||
|
isArray: z.boolean(),
|
||||||
|
defaultValue: z.string().optional(),
|
||||||
|
relation: z.string().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
const prismaModelSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
fields: z.array(prismaFieldSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeSchema = z.object({
|
||||||
|
primaryColor: z.string(),
|
||||||
|
secondaryColor: z.string(),
|
||||||
|
errorColor: z.string(),
|
||||||
|
warningColor: z.string(),
|
||||||
|
successColor: z.string(),
|
||||||
|
fontFamily: z.string(),
|
||||||
|
fontSize: z.object({
|
||||||
|
small: z.number(),
|
||||||
|
medium: z.number(),
|
||||||
|
large: z.number()
|
||||||
|
}),
|
||||||
|
spacing: z.number(),
|
||||||
|
borderRadius: z.number()
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectFileSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
language: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const componentResponseSchema = z.object({ component: componentNodeSchema })
|
||||||
|
const prismaModelResponseSchema = z.object({ model: prismaModelSchema })
|
||||||
|
const themeResponseSchema = z.object({ theme: themeSchema })
|
||||||
|
const suggestFieldsResponseSchema = z.object({ fields: z.array(z.string()) })
|
||||||
|
const completeAppResponseSchema = z.object({
|
||||||
|
files: z.array(projectFileSchema),
|
||||||
|
models: z.array(prismaModelSchema),
|
||||||
|
theme: themeSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseAndValidateJson = <T,>(
|
||||||
|
result: string,
|
||||||
|
schema: z.ZodType<T>,
|
||||||
|
context: string,
|
||||||
|
toastMessage: string
|
||||||
|
): T | null => {
|
||||||
|
let parsed: unknown
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI response JSON parse failed', {
|
||||||
|
context,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
rawResponse: result
|
||||||
|
})
|
||||||
|
toast.error(toastMessage)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = schema.safeParse(parsed)
|
||||||
|
if (!validation.success) {
|
||||||
|
console.error('AI response validation failed', {
|
||||||
|
context,
|
||||||
|
issues: validation.error.issues,
|
||||||
|
rawResponse: parsed
|
||||||
|
})
|
||||||
|
toast.error(toastMessage)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.data
|
||||||
|
}
|
||||||
|
|
||||||
export class AIService {
|
export class AIService {
|
||||||
static async generateComponent(description: string): Promise<ComponentNode | null> {
|
static async generateComponent(description: string): Promise<ComponentNode | null> {
|
||||||
@@ -29,8 +124,13 @@ Make sure to use appropriate Material UI components and props. Keep the structur
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = JSON.parse(result)
|
const parsed = parseAndValidateJson(
|
||||||
return parsed.component
|
result,
|
||||||
|
componentResponseSchema,
|
||||||
|
'generate-component',
|
||||||
|
'AI component response was invalid. Please retry or clarify your description.'
|
||||||
|
)
|
||||||
|
return parsed ? parsed.component : null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -80,8 +180,13 @@ Return a valid JSON object with a single property "model" containing the model s
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = JSON.parse(result)
|
const parsed = parseAndValidateJson(
|
||||||
return parsed.model
|
result,
|
||||||
|
prismaModelResponseSchema,
|
||||||
|
'generate-model',
|
||||||
|
'AI model response was invalid. Please retry or describe the model differently.'
|
||||||
|
)
|
||||||
|
return parsed ? parsed.model : null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -172,8 +277,13 @@ Return a valid JSON object with a single property "theme" containing:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = JSON.parse(result)
|
const parsed = parseAndValidateJson(
|
||||||
return parsed.theme
|
result,
|
||||||
|
themeResponseSchema,
|
||||||
|
'generate-theme',
|
||||||
|
'AI theme response was invalid. Please retry or specify the theme requirements.'
|
||||||
|
)
|
||||||
|
return parsed ? parsed.theme : null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -202,8 +312,13 @@ Suggest 3-5 common fields that would be useful for this model type. Use camelCas
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = JSON.parse(result)
|
const parsed = parseAndValidateJson(
|
||||||
return parsed.fields
|
result,
|
||||||
|
suggestFieldsResponseSchema,
|
||||||
|
'suggest-fields',
|
||||||
|
'AI field suggestions were invalid. Please retry with a clearer model name.'
|
||||||
|
)
|
||||||
|
return parsed ? parsed.fields : null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -284,7 +399,12 @@ Create 2-4 essential files for the app structure. Include appropriate Prisma mod
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return JSON.parse(result)
|
return parseAndValidateJson(
|
||||||
|
result,
|
||||||
|
completeAppResponseSchema,
|
||||||
|
'generate-app',
|
||||||
|
'AI app generation response was invalid. Please retry with more detail.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
52
src/lib/json-ui/__tests__/component-registry.test.ts
Normal file
52
src/lib/json-ui/__tests__/component-registry.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import jsonComponentsRegistry from '../../../../json-components-registry.json'
|
||||||
|
import { getUIComponent } from '../component-registry'
|
||||||
|
|
||||||
|
type JsonRegistryEntry = {
|
||||||
|
type?: string
|
||||||
|
name?: string
|
||||||
|
status?: string
|
||||||
|
source?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonComponentRegistry = {
|
||||||
|
components?: JsonRegistryEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = jsonComponentsRegistry as JsonComponentRegistry
|
||||||
|
const registryEntries = registry.components ?? []
|
||||||
|
|
||||||
|
const allowlistedMissingComponents = new Map<string, string>([])
|
||||||
|
|
||||||
|
const getTellTaleEntryKey = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.type ?? entry.name
|
||||||
|
|
||||||
|
describe('json component registry coverage', () => {
|
||||||
|
it('resolves every registry entry to a UI component or allowlisted exception', () => {
|
||||||
|
for (const entry of registryEntries) {
|
||||||
|
const type = getTellTaleEntryKey(entry)
|
||||||
|
if (!type) {
|
||||||
|
throw new Error(
|
||||||
|
`Registry entry missing type/name. Status: ${entry.status ?? 'unknown'} Source: ${
|
||||||
|
entry.source ?? 'unknown'
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = getUIComponent(type)
|
||||||
|
if (!component) {
|
||||||
|
const allowlistedReason = allowlistedMissingComponents.get(type)
|
||||||
|
if (allowlistedReason) {
|
||||||
|
expect(
|
||||||
|
component,
|
||||||
|
`Allowlisted missing component should stay null: ${type}. Reason: ${allowlistedReason}`
|
||||||
|
).toBeNull()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
throw new Error(`Missing UI component for registry type "${type}".`)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(component, `Registry type "${type}" should resolve to a component.`).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,63 +1,6 @@
|
|||||||
import { ComponentType } from 'react'
|
import { ComponentType } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import * as PhosphorIcons from '@phosphor-icons/react'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { InputOtp } from '@/components/ui/input-otp'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { Alert as ShadcnAlert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
|
||||||
import { AlertDialog } from '@/components/ui/alert-dialog'
|
|
||||||
import { AspectRatio } from '@/components/ui/aspect-ratio'
|
|
||||||
import { Carousel } from '@/components/ui/carousel'
|
|
||||||
import { ChartContainer as Chart } from '@/components/ui/chart'
|
|
||||||
import { Collapsible } from '@/components/ui/collapsible'
|
|
||||||
import { Command } from '@/components/ui/command'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
||||||
import { DropdownMenu } from '@/components/ui/dropdown-menu'
|
|
||||||
import { Menubar } from '@/components/ui/menubar'
|
|
||||||
import { NavigationMenu } from '@/components/ui/navigation-menu'
|
|
||||||
import { Table as ShadcnTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
||||||
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
|
||||||
import { ResizablePanelGroup as Resizable } from '@/components/ui/resizable'
|
|
||||||
import { Sheet } from '@/components/ui/sheet'
|
|
||||||
import { Sidebar } from '@/components/ui/sidebar'
|
|
||||||
import { Toaster as Sonner } from '@/components/ui/sonner'
|
|
||||||
import { ToggleGroup } from '@/components/ui/toggle-group'
|
|
||||||
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
|
||||||
import * as AtomComponents from '@/components/atoms'
|
|
||||||
import * as MoleculeComponents from '@/components/molecules'
|
|
||||||
import * as OrganismComponents from '@/components/organisms'
|
|
||||||
import {
|
|
||||||
ComponentBindingDialogWrapper,
|
|
||||||
ComponentTreeWrapper,
|
|
||||||
DataSourceEditorDialogWrapper,
|
|
||||||
GitHubBuildStatusWrapper,
|
|
||||||
LazyBarChartWrapper,
|
|
||||||
LazyD3BarChartWrapper,
|
|
||||||
LazyLineChartWrapper,
|
|
||||||
SaveIndicatorWrapper,
|
|
||||||
SeedDataManagerWrapper,
|
|
||||||
StorageSettingsWrapper,
|
|
||||||
} from '@/lib/json-ui/wrappers'
|
|
||||||
import jsonComponentsRegistry from '../../../json-components-registry.json'
|
import jsonComponentsRegistry from '../../../json-components-registry.json'
|
||||||
import {
|
|
||||||
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
|
|
||||||
Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed,
|
|
||||||
CaretUp, CaretDown, CaretLeft, CaretRight,
|
|
||||||
Gear, User, Bell, Envelope, Calendar, Clock, Star,
|
|
||||||
Heart, ShareNetwork, LinkSimple, Copy, FloppyDisk, ArrowClockwise, WarningCircle,
|
|
||||||
Info, Question, House, List as ListIcon, DotsThreeVertical, DotsThree
|
|
||||||
} from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
export interface UIComponentRegistry {
|
export interface UIComponentRegistry {
|
||||||
[key: string]: ComponentType<any>
|
[key: string]: ComponentType<any>
|
||||||
@@ -72,11 +15,16 @@ interface JsonRegistryEntry {
|
|||||||
wrapperRequired?: boolean
|
wrapperRequired?: boolean
|
||||||
wrapperComponent?: string
|
wrapperComponent?: string
|
||||||
wrapperFor?: string
|
wrapperFor?: string
|
||||||
|
load?: {
|
||||||
|
path?: string
|
||||||
|
export?: string
|
||||||
|
}
|
||||||
deprecated?: DeprecatedComponentInfo
|
deprecated?: DeprecatedComponentInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JsonComponentRegistry {
|
interface JsonComponentRegistry {
|
||||||
components?: JsonRegistryEntry[]
|
components?: JsonRegistryEntry[]
|
||||||
|
sourceRoots?: Record<string, string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeprecatedComponentInfo {
|
export interface DeprecatedComponentInfo {
|
||||||
@@ -85,70 +33,127 @@ export interface DeprecatedComponentInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
||||||
|
const sourceRoots = jsonRegistry.sourceRoots ?? {}
|
||||||
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
|
const moduleMapsBySource = Object.fromEntries(
|
||||||
entry.export ?? entry.name ?? entry.type
|
Object.entries(sourceRoots).map(([source, patterns]) => {
|
||||||
|
if (!patterns || patterns.length === 0) {
|
||||||
const buildRegistryFromNames = (
|
return [source, {}]
|
||||||
names: string[],
|
|
||||||
components: Record<string, ComponentType<any>>
|
|
||||||
): UIComponentRegistry => {
|
|
||||||
return names.reduce<UIComponentRegistry>((registry, name) => {
|
|
||||||
const component = components[name]
|
|
||||||
if (component) {
|
|
||||||
registry[name] = component
|
|
||||||
}
|
}
|
||||||
return registry
|
return [source, import.meta.glob(patterns, { eager: true })]
|
||||||
}, {})
|
})
|
||||||
}
|
) as Record<string, Record<string, unknown>>
|
||||||
|
|
||||||
|
const getRegistryEntryKey = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.name ?? entry.type
|
||||||
|
|
||||||
|
const getRegistryEntryExportName = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
|
entry.load?.export ?? entry.export ?? getRegistryEntryKey(entry)
|
||||||
|
|
||||||
const jsonRegistryEntries = jsonRegistry.components ?? []
|
const jsonRegistryEntries = jsonRegistry.components ?? []
|
||||||
const registryEntryByType = new Map(
|
const registryEntryByType = new Map(
|
||||||
jsonRegistryEntries
|
jsonRegistryEntries
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const entryName = getRegistryEntryName(entry)
|
const entryKey = getRegistryEntryKey(entry)
|
||||||
return entryName ? [entryName, entry] : null
|
return entryKey ? [entryKey, entry] : null
|
||||||
})
|
})
|
||||||
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
|
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
|
||||||
)
|
)
|
||||||
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 = getRegistryEntryName(entry)
|
const entryKey = getRegistryEntryKey(entry)
|
||||||
if (!entryName) {
|
if (!entryKey) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
if (entry.status === 'deprecated' || entry.deprecated) {
|
if (entry.status === 'deprecated' || entry.deprecated) {
|
||||||
acc[entryName] = entry.deprecated ?? {}
|
acc[entryKey] = entry.deprecated ?? {}
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
const atomRegistryNames = jsonRegistryEntries
|
|
||||||
.filter((entry) => entry.source === 'atoms')
|
const buildComponentMapFromExports = (
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
exports: Record<string, unknown>
|
||||||
.filter((name): name is string => Boolean(name))
|
): Record<string, ComponentType<any>> => {
|
||||||
const moleculeRegistryNames = jsonRegistryEntries
|
return Object.entries(exports).reduce<Record<string, ComponentType<any>>>((acc, [key, value]) => {
|
||||||
.filter((entry) => entry.source === 'molecules')
|
if (value && (typeof value === 'function' || typeof value === 'object')) {
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
acc[key] = value as ComponentType<any>
|
||||||
.filter((name): name is string => Boolean(name))
|
}
|
||||||
const organismRegistryNames = jsonRegistryEntries
|
return acc
|
||||||
.filter((entry) => entry.source === 'organisms')
|
}, {})
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
}
|
||||||
.filter((name): name is string => Boolean(name))
|
|
||||||
const shadcnRegistryNames = jsonRegistryEntries
|
const buildComponentMapFromModules = (
|
||||||
.filter((entry) => entry.source === 'ui')
|
modules: Record<string, unknown>
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
): Record<string, ComponentType<any>> => {
|
||||||
.filter((name): name is string => Boolean(name))
|
return Object.values(modules).reduce<Record<string, ComponentType<any>>>((acc, moduleExports) => {
|
||||||
const wrapperRegistryNames = jsonRegistryEntries
|
if (!moduleExports || typeof moduleExports !== 'object') {
|
||||||
.filter((entry) => entry.source === 'wrappers')
|
return acc
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
}
|
||||||
.filter((name): name is string => Boolean(name))
|
Object.entries(buildComponentMapFromExports(moduleExports as Record<string, unknown>)).forEach(
|
||||||
const iconRegistryNames = jsonRegistryEntries
|
([key, component]) => {
|
||||||
.filter((entry) => entry.source === 'icons')
|
acc[key] = component
|
||||||
.map((entry) => getRegistryEntryName(entry))
|
}
|
||||||
.filter((name): name is string => Boolean(name))
|
)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const atomModules = import.meta.glob('@/components/atoms/*.tsx', { eager: true })
|
||||||
|
const moleculeModules = import.meta.glob('@/components/molecules/*.tsx', { eager: true })
|
||||||
|
const organismModules = import.meta.glob('@/components/organisms/*.tsx', { eager: true })
|
||||||
|
const uiModules = import.meta.glob('@/components/ui/**/*.{ts,tsx}', { eager: true })
|
||||||
|
const wrapperModules = import.meta.glob('@/lib/json-ui/wrappers/*.tsx', { eager: true })
|
||||||
|
const explicitModules = import.meta.glob(
|
||||||
|
['@/components/**/*.tsx', '@/lib/json-ui/wrappers/**/*.tsx'],
|
||||||
|
{ eager: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const atomComponentMap = buildComponentMapFromModules(atomModules)
|
||||||
|
const moleculeComponentMap = buildComponentMapFromModules(moleculeModules)
|
||||||
|
const organismComponentMap = buildComponentMapFromModules(organismModules)
|
||||||
|
const uiComponentMap = buildComponentMapFromModules(uiModules)
|
||||||
|
const wrapperComponentMap = buildComponentMapFromModules(wrapperModules)
|
||||||
|
const iconComponentMap = buildComponentMapFromExports(PhosphorIcons)
|
||||||
|
|
||||||
|
const resolveComponentFromExplicitPath = (
|
||||||
|
entry: JsonRegistryEntry,
|
||||||
|
entryExportName: string
|
||||||
|
): ComponentType<any> | undefined => {
|
||||||
|
if (!entry.load?.path) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const moduleExports = explicitModules[entry.load.path]
|
||||||
|
if (!moduleExports || typeof moduleExports !== 'object') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const explicitComponents = buildComponentMapFromExports(
|
||||||
|
moduleExports as Record<string, unknown>
|
||||||
|
)
|
||||||
|
return explicitComponents[entryExportName]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildRegistryFromEntries = (
|
||||||
|
source: string,
|
||||||
|
componentMap: Record<string, ComponentType<any>>
|
||||||
|
): UIComponentRegistry => {
|
||||||
|
return jsonRegistryEntries
|
||||||
|
.filter((entry) => entry.source === source)
|
||||||
|
.reduce<UIComponentRegistry>((registry, entry) => {
|
||||||
|
const entryKey = getRegistryEntryKey(entry)
|
||||||
|
const entryExportName = getRegistryEntryExportName(entry)
|
||||||
|
if (!entryKey || !entryExportName) {
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
const component =
|
||||||
|
resolveComponentFromExplicitPath(entry, entryExportName) ??
|
||||||
|
componentMap[entryExportName]
|
||||||
|
if (component) {
|
||||||
|
registry[entryKey] = component
|
||||||
|
}
|
||||||
|
return registry
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
export const primitiveComponents: UIComponentRegistry = {
|
export const primitiveComponents: UIComponentRegistry = {
|
||||||
div: 'div' as any,
|
div: 'div' as any,
|
||||||
@@ -169,173 +174,33 @@ export const primitiveComponents: UIComponentRegistry = {
|
|||||||
nav: 'nav' as any,
|
nav: 'nav' as any,
|
||||||
}
|
}
|
||||||
|
|
||||||
const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
export const shadcnComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
AlertDialog,
|
'ui',
|
||||||
AspectRatio,
|
uiComponentMap
|
||||||
Button,
|
|
||||||
Carousel,
|
|
||||||
Chart,
|
|
||||||
Collapsible,
|
|
||||||
Command,
|
|
||||||
DropdownMenu,
|
|
||||||
Input,
|
|
||||||
InputOtp,
|
|
||||||
Textarea,
|
|
||||||
Label,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
Badge,
|
|
||||||
Separator,
|
|
||||||
Alert: ShadcnAlert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertTitle,
|
|
||||||
Switch,
|
|
||||||
Checkbox,
|
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
Table: ShadcnTable,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
Menubar,
|
|
||||||
NavigationMenu,
|
|
||||||
Skeleton: ShadcnSkeleton,
|
|
||||||
Pagination,
|
|
||||||
Progress,
|
|
||||||
Resizable,
|
|
||||||
Sheet,
|
|
||||||
Sidebar,
|
|
||||||
Sonner,
|
|
||||||
ToggleGroup,
|
|
||||||
Avatar: ShadcnAvatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shadcnComponents: UIComponentRegistry = buildRegistryFromNames(
|
|
||||||
shadcnRegistryNames,
|
|
||||||
shadcnComponentMap
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const atomComponents: UIComponentRegistry = {
|
export const atomComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
...buildRegistryFromNames(
|
'atoms',
|
||||||
atomRegistryNames,
|
atomComponentMap
|
||||||
atomComponentMap
|
|
||||||
),
|
|
||||||
DatePicker: atomComponentMap.DatePicker,
|
|
||||||
FileUpload: atomComponentMap.FileUpload,
|
|
||||||
CircularProgress,
|
|
||||||
Divider,
|
|
||||||
ProgressBar,
|
|
||||||
DataList: (AtomComponents as Record<string, ComponentType<any>>).DataList,
|
|
||||||
DataTable: (AtomComponents as Record<string, ComponentType<any>>).DataTable,
|
|
||||||
ListItem: (AtomComponents as Record<string, ComponentType<any>>).ListItem,
|
|
||||||
MetricCard: (AtomComponents as Record<string, ComponentType<any>>).MetricCard,
|
|
||||||
Timeline: (AtomComponents as Record<string, ComponentType<any>>).Timeline,
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbComponent = AtomComponents.Breadcrumb ?? AtomComponents.BreadcrumbNav
|
|
||||||
if (breadcrumbComponent) {
|
|
||||||
atomComponents.Breadcrumb = breadcrumbComponent as ComponentType<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const moleculeComponents: UIComponentRegistry = {
|
|
||||||
...buildRegistryFromNames(
|
|
||||||
moleculeRegistryNames,
|
|
||||||
MoleculeComponents as Record<string, ComponentType<any>>
|
|
||||||
),
|
|
||||||
AppBranding: (MoleculeComponents as Record<string, ComponentType<any>>).AppBranding,
|
|
||||||
LabelWithBadge: (MoleculeComponents as Record<string, ComponentType<any>>).LabelWithBadge,
|
|
||||||
NavigationGroupHeader: (MoleculeComponents as Record<string, ComponentType<any>>).NavigationGroupHeader,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
|
|
||||||
organismRegistryNames,
|
|
||||||
OrganismComponents as Record<string, ComponentType<any>>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const wrapperComponentMap: Record<string, ComponentType<any>> = {
|
export const moleculeComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
ComponentBindingDialogWrapper,
|
'molecules',
|
||||||
ComponentTreeWrapper,
|
moleculeComponentMap
|
||||||
DataSourceEditorDialogWrapper,
|
)
|
||||||
GitHubBuildStatusWrapper,
|
|
||||||
SaveIndicatorWrapper,
|
|
||||||
LazyBarChartWrapper,
|
|
||||||
LazyLineChartWrapper,
|
|
||||||
LazyD3BarChartWrapper,
|
|
||||||
SeedDataManagerWrapper,
|
|
||||||
StorageSettingsWrapper,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromNames(
|
export const organismComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
wrapperRegistryNames,
|
'organisms',
|
||||||
|
organismComponentMap
|
||||||
|
)
|
||||||
|
|
||||||
|
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
|
'wrappers',
|
||||||
wrapperComponentMap
|
wrapperComponentMap
|
||||||
)
|
)
|
||||||
|
|
||||||
const iconComponentMap: Record<string, ComponentType<any>> = {
|
export const iconComponents: UIComponentRegistry = buildRegistryFromEntries(
|
||||||
ArrowLeft,
|
'icons',
|
||||||
ArrowRight,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
Plus,
|
|
||||||
Minus,
|
|
||||||
Search: MagnifyingGlass,
|
|
||||||
Filter: Funnel,
|
|
||||||
Download,
|
|
||||||
Upload,
|
|
||||||
Edit: PencilSimple,
|
|
||||||
Trash,
|
|
||||||
Eye,
|
|
||||||
EyeOff: EyeClosed,
|
|
||||||
ChevronUp: CaretUp,
|
|
||||||
ChevronDown: CaretDown,
|
|
||||||
ChevronLeft: CaretLeft,
|
|
||||||
ChevronRight: CaretRight,
|
|
||||||
Settings: Gear,
|
|
||||||
User,
|
|
||||||
Bell,
|
|
||||||
Mail: Envelope,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
Star,
|
|
||||||
Heart,
|
|
||||||
Share: ShareNetwork,
|
|
||||||
Link: LinkSimple,
|
|
||||||
Copy,
|
|
||||||
Save: FloppyDisk,
|
|
||||||
RefreshCw: ArrowClockwise,
|
|
||||||
AlertCircle: WarningCircle,
|
|
||||||
Info,
|
|
||||||
HelpCircle: Question,
|
|
||||||
Home: House,
|
|
||||||
Menu: ListIcon,
|
|
||||||
MoreVertical: DotsThreeVertical,
|
|
||||||
MoreHorizontal: DotsThree,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const iconComponents: UIComponentRegistry = buildRegistryFromNames(
|
|
||||||
iconRegistryNames,
|
|
||||||
iconComponentMap
|
iconComponentMap
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,35 @@ export function evaluateExpression(
|
|||||||
return lengthSuffix ? filtered.length : filtered
|
return lengthSuffix ? filtered.length : filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findMatch = expression.match(
|
||||||
|
/^data\.([a-zA-Z0-9_.]+)\.find\(\s*([a-zA-Z0-9_.]+)\s*(===|==|!==|!=)\s*(.+?)\s*\)$/
|
||||||
|
)
|
||||||
|
if (findMatch) {
|
||||||
|
const [, collectionPath, fieldPath, operator, rawValue] = findMatch
|
||||||
|
const collection = getNestedValue(data, collectionPath)
|
||||||
|
if (!Array.isArray(collection)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedValue = evaluateExpression(rawValue.trim(), { data, event })
|
||||||
|
const isNegated = operator === '!=' || operator === '!=='
|
||||||
|
return collection.find((item) => {
|
||||||
|
const fieldValue = getNestedValue(item, fieldPath)
|
||||||
|
return isNegated ? fieldValue !== expectedValue : fieldValue === expectedValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectKeysLengthMatch = expression.match(
|
||||||
|
/^Object\.keys\(\s*data\.([a-zA-Z0-9_.]+)\s*\)\.length$/
|
||||||
|
)
|
||||||
|
if (objectKeysLengthMatch) {
|
||||||
|
const value = getNestedValue(data, objectKeysLengthMatch[1])
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Object.keys(value).length
|
||||||
|
}
|
||||||
|
|
||||||
// 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))
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export function useJSONDataSource<T = unknown>(
|
|||||||
) {
|
) {
|
||||||
const kvConfig = config.type === 'kv' ? config.config : undefined
|
const kvConfig = config.type === 'kv' ? config.config : undefined
|
||||||
const apiConfig = config.type === 'api' ? config.config : undefined
|
const apiConfig = config.type === 'api' ? config.config : undefined
|
||||||
const computedConfig = config.type === 'computed' ? config.config : undefined
|
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
config.type === 'static' ? config.config : config.config?.defaultValue
|
config.type === 'static' ? config.config : config.config?.defaultValue
|
||||||
|
|
||||||
@@ -57,8 +56,6 @@ export function useJSONDataSource<T = unknown>(
|
|||||||
return apiValue
|
return apiValue
|
||||||
case 'static':
|
case 'static':
|
||||||
return config.config
|
return config.config
|
||||||
case 'computed':
|
|
||||||
return computedConfig?.defaultValue
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export const PageUISchema = z.object({
|
|||||||
tables: z.array(TableSchema).optional(),
|
tables: z.array(TableSchema).optional(),
|
||||||
menus: z.array(MenuSchema).optional(),
|
menus: z.array(MenuSchema).optional(),
|
||||||
dataSources: z.record(z.string(), z.object({
|
dataSources: z.record(z.string(), z.object({
|
||||||
type: z.enum(['kv', 'api', 'computed', 'static']),
|
type: z.enum(['kv', 'api', 'static']),
|
||||||
config: z.any(),
|
config: z.any(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
})
|
})
|
||||||
@@ -241,13 +241,6 @@ export type DataSourceConfig<T = unknown> =
|
|||||||
transform?: (data: unknown) => T
|
transform?: (data: unknown) => T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'computed'
|
|
||||||
config: {
|
|
||||||
defaultValue?: T
|
|
||||||
transform?: (data: unknown) => T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'static'
|
type: 'static'
|
||||||
config: T
|
config: T
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
import { StatusIcon } from '@/components/atoms'
|
import { StatusIcon } from '@/components/atoms'
|
||||||
|
import { useSaveIndicator } from '@/hooks/use-save-indicator'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { SaveIndicatorWrapperProps } from './interfaces'
|
import type { SaveIndicatorWrapperProps } from './interfaces'
|
||||||
|
|
||||||
export function SaveIndicatorWrapper({
|
export function SaveIndicatorWrapper({
|
||||||
|
lastSaved,
|
||||||
status = 'saved',
|
status = 'saved',
|
||||||
label,
|
label,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
animate,
|
animate,
|
||||||
className,
|
className,
|
||||||
}: SaveIndicatorWrapperProps) {
|
}: SaveIndicatorWrapperProps) {
|
||||||
|
const { timeAgo, isRecent } = useSaveIndicator(lastSaved ?? null)
|
||||||
|
|
||||||
|
if (lastSaved) {
|
||||||
|
const resolvedStatus = isRecent ? 'saved' : 'synced'
|
||||||
|
const resolvedLabel = label ?? (isRecent ? 'Saved' : timeAgo)
|
||||||
|
const shouldAnimate = animate ?? isRecent
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}>
|
||||||
|
<StatusIcon type={resolvedStatus} animate={shouldAnimate} />
|
||||||
|
{showLabel && <span className="hidden sm:inline">{resolvedLabel}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedLabel = label ?? (status === 'saved' ? 'Saved' : 'Synced')
|
const resolvedLabel = label ?? (status === 'saved' ? 'Saved' : 'Synced')
|
||||||
const shouldAnimate = animate ?? status === 'saved'
|
const shouldAnimate = animate ?? status === 'saved'
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { UIComponent } from '@/types/json-ui'
|
|||||||
export type SaveIndicatorStatus = 'saved' | 'synced'
|
export type SaveIndicatorStatus = 'saved' | 'synced'
|
||||||
|
|
||||||
export interface SaveIndicatorWrapperProps {
|
export interface SaveIndicatorWrapperProps {
|
||||||
|
lastSaved?: number | null
|
||||||
status?: SaveIndicatorStatus
|
status?: SaveIndicatorStatus
|
||||||
label?: string
|
label?: string
|
||||||
showLabel?: boolean
|
showLabel?: boolean
|
||||||
|
|||||||
56
src/lib/rate-limiter.test.ts
Normal file
56
src/lib/rate-limiter.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { RateLimiter } from './rate-limiter'
|
||||||
|
|
||||||
|
describe('RateLimiter.throttle', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null when the window is saturated for medium priority', async () => {
|
||||||
|
const limiter = new RateLimiter({
|
||||||
|
maxRequests: 1,
|
||||||
|
windowMs: 1000,
|
||||||
|
retryDelay: 10,
|
||||||
|
maxRetries: 2
|
||||||
|
})
|
||||||
|
const fn = vi.fn(async () => 'ok')
|
||||||
|
|
||||||
|
await limiter.throttle('key', fn, 'medium')
|
||||||
|
const result = await limiter.throttle('key', fn, 'medium')
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bounds high-priority retries without recursion when the window is saturated', async () => {
|
||||||
|
const limiter = new RateLimiter({
|
||||||
|
maxRequests: 1,
|
||||||
|
windowMs: 1000,
|
||||||
|
retryDelay: 10,
|
||||||
|
maxRetries: 3
|
||||||
|
})
|
||||||
|
const fn = vi.fn(async () => 'ok')
|
||||||
|
|
||||||
|
await limiter.throttle('key', fn, 'high')
|
||||||
|
|
||||||
|
const spy = vi.spyOn(limiter, 'throttle')
|
||||||
|
let resolved: unknown = 'pending'
|
||||||
|
const pending = limiter.throttle('key', fn, 'high').then(result => {
|
||||||
|
resolved = result
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(30)
|
||||||
|
await pending
|
||||||
|
|
||||||
|
expect(resolved).toBeNull()
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@ interface RateLimitConfig {
|
|||||||
maxRequests: number
|
maxRequests: number
|
||||||
windowMs: number
|
windowMs: number
|
||||||
retryDelay: number
|
retryDelay: number
|
||||||
|
maxRetries?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestRecord {
|
interface RequestRecord {
|
||||||
@@ -9,14 +10,15 @@ interface RequestRecord {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class RateLimiter {
|
export class RateLimiter {
|
||||||
private requests: Map<string, RequestRecord> = new Map()
|
private requests: Map<string, RequestRecord> = new Map()
|
||||||
private config: RateLimitConfig
|
private config: RateLimitConfig
|
||||||
|
|
||||||
constructor(config: RateLimitConfig = {
|
constructor(config: RateLimitConfig = {
|
||||||
maxRequests: 5,
|
maxRequests: 5,
|
||||||
windowMs: 60000,
|
windowMs: 60000,
|
||||||
retryDelay: 2000
|
retryDelay: 2000,
|
||||||
|
maxRetries: 3
|
||||||
}) {
|
}) {
|
||||||
this.config = config
|
this.config = config
|
||||||
}
|
}
|
||||||
@@ -26,49 +28,60 @@ class RateLimiter {
|
|||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
priority: 'low' | 'medium' | 'high' = 'medium'
|
priority: 'low' | 'medium' | 'high' = 'medium'
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
const now = Date.now()
|
const maxRetries = this.config.maxRetries ?? 3
|
||||||
const record = this.requests.get(key)
|
let attempts = 0
|
||||||
|
|
||||||
if (record) {
|
while (true) {
|
||||||
const timeElapsed = now - record.timestamp
|
const now = Date.now()
|
||||||
|
const record = this.requests.get(key)
|
||||||
|
let isLimited = false
|
||||||
|
|
||||||
if (timeElapsed < this.config.windowMs) {
|
if (record) {
|
||||||
if (record.count >= this.config.maxRequests) {
|
const timeElapsed = now - record.timestamp
|
||||||
console.warn(`Rate limit exceeded for ${key}. Try again in ${Math.ceil((this.config.windowMs - timeElapsed) / 1000)}s`)
|
|
||||||
|
if (timeElapsed < this.config.windowMs) {
|
||||||
if (priority === 'high') {
|
if (record.count >= this.config.maxRequests) {
|
||||||
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay))
|
console.warn(`Rate limit exceeded for ${key}. Try again in ${Math.ceil((this.config.windowMs - timeElapsed) / 1000)}s`)
|
||||||
return this.throttle(key, fn, priority)
|
isLimited = true
|
||||||
|
} else {
|
||||||
|
record.count++
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return null
|
this.requests.set(key, { timestamp: now, count: 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
record.count++
|
|
||||||
} else {
|
} else {
|
||||||
this.requests.set(key, { timestamp: now, count: 1 })
|
this.requests.set(key, { timestamp: now, count: 1 })
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.requests.set(key, { timestamp: now, count: 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
|
|
||||||
try {
|
if (isLimited) {
|
||||||
return await fn()
|
if (priority === 'high' && attempts < maxRetries) {
|
||||||
} catch (error) {
|
attempts += 1
|
||||||
if (error instanceof Error && (
|
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay))
|
||||||
error.message.includes('502') ||
|
continue
|
||||||
error.message.includes('Bad Gateway') ||
|
|
||||||
error.message.includes('429') ||
|
|
||||||
error.message.includes('rate limit')
|
|
||||||
)) {
|
|
||||||
console.error(`Gateway error for ${key}:`, error.message)
|
|
||||||
if (record) {
|
|
||||||
record.count = this.config.maxRequests
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && (
|
||||||
|
error.message.includes('502') ||
|
||||||
|
error.message.includes('Bad Gateway') ||
|
||||||
|
error.message.includes('429') ||
|
||||||
|
error.message.includes('rate limit')
|
||||||
|
)) {
|
||||||
|
console.error(`Gateway error for ${key}:`, error.message)
|
||||||
|
const updatedRecord = this.requests.get(key)
|
||||||
|
if (updatedRecord) {
|
||||||
|
updatedRecord.count = this.config.maxRequests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,22 +24,18 @@ export class FlaskBackendAdapter implements StorageAdapter {
|
|||||||
|
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
|
const contentLength = response.headers.get('content-length')
|
||||||
|
const contentType = response.headers.get('content-type')
|
||||||
|
const hasJsonBody = contentLength !== '0' && contentType?.includes('application/json')
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = response.statusText
|
const errorPayload = hasJsonBody ? await response.json().catch(() => null) : null
|
||||||
try {
|
const errorMessage = errorPayload?.error || response.statusText || `HTTP ${response.status}`
|
||||||
const errorText = await response.text()
|
throw new Error(errorMessage)
|
||||||
if (errorText) {
|
}
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(errorText) as { error?: string }
|
if (response.status === 204 || !hasJsonBody) {
|
||||||
errorMessage = parsed.error || errorText
|
return undefined as T
|
||||||
} catch {
|
|
||||||
errorMessage = errorText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore error parsing failures
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage || `HTTP ${response.status}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseText = await response.text()
|
const responseText = await response.text()
|
||||||
|
|||||||
@@ -22,13 +22,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filteredUsers",
|
"id": "filteredUsers",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"expression": "data.users",
|
"expression": "data.users",
|
||||||
"dependencies": ["users", "filterQuery"]
|
"dependencies": ["users", "filterQuery"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"valueTemplate": {
|
"valueTemplate": {
|
||||||
"total": "data.users.length",
|
"total": "data.users.length",
|
||||||
"active": "data.users.filter(status === 'active').length",
|
"active": "data.users.filter(status === 'active').length",
|
||||||
|
|||||||
300
src/schemas/feature-toggle-settings.json
Normal file
300
src/schemas/feature-toggle-settings.json
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
{
|
||||||
|
"id": "feature-toggle-settings",
|
||||||
|
"name": "Feature Toggle Settings",
|
||||||
|
"description": "Enable or disable features to customize your workspace",
|
||||||
|
"dataSources": [
|
||||||
|
{
|
||||||
|
"id": "featuresList",
|
||||||
|
"type": "static",
|
||||||
|
"defaultValue": [
|
||||||
|
{
|
||||||
|
"key": "codeEditor",
|
||||||
|
"label": "Code Editor",
|
||||||
|
"description": "Monaco-based code editor with syntax highlighting",
|
||||||
|
"icon": "Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "models",
|
||||||
|
"label": "Database Models",
|
||||||
|
"description": "Prisma schema designer for database models",
|
||||||
|
"icon": "Database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "components",
|
||||||
|
"label": "Component Builder",
|
||||||
|
"description": "Visual component tree builder for React components",
|
||||||
|
"icon": "Tree"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "componentTrees",
|
||||||
|
"label": "Component Trees Manager",
|
||||||
|
"description": "Manage multiple component tree configurations",
|
||||||
|
"icon": "Tree"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "workflows",
|
||||||
|
"label": "Workflow Designer",
|
||||||
|
"description": "n8n-style visual workflow automation builder",
|
||||||
|
"icon": "FlowArrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "lambdas",
|
||||||
|
"label": "Lambda Functions",
|
||||||
|
"description": "Serverless function editor with multiple runtimes",
|
||||||
|
"icon": "Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "styling",
|
||||||
|
"label": "Theme Designer",
|
||||||
|
"description": "Material UI theme customization and styling",
|
||||||
|
"icon": "PaintBrush"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "flaskApi",
|
||||||
|
"label": "Flask API Designer",
|
||||||
|
"description": "Python Flask backend API endpoint designer",
|
||||||
|
"icon": "Flask"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "playwright",
|
||||||
|
"label": "Playwright Tests",
|
||||||
|
"description": "E2E testing with Playwright configuration",
|
||||||
|
"icon": "Play"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "storybook",
|
||||||
|
"label": "Storybook Stories",
|
||||||
|
"description": "Component documentation and development",
|
||||||
|
"icon": "BookOpen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "unitTests",
|
||||||
|
"label": "Unit Tests",
|
||||||
|
"description": "Component and function unit test designer",
|
||||||
|
"icon": "Cube"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "errorRepair",
|
||||||
|
"label": "Error Repair",
|
||||||
|
"description": "Auto-detect and fix code errors",
|
||||||
|
"icon": "Wrench"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "documentation",
|
||||||
|
"label": "Documentation",
|
||||||
|
"description": "Project documentation, roadmap, and guides",
|
||||||
|
"icon": "FileText"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sassStyles",
|
||||||
|
"label": "Sass Styles",
|
||||||
|
"description": "Custom Sass/SCSS styling showcase",
|
||||||
|
"icon": "PaintBrush"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "faviconDesigner",
|
||||||
|
"label": "Favicon Designer",
|
||||||
|
"description": "Design and generate app favicons and icons",
|
||||||
|
"icon": "Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ideaCloud",
|
||||||
|
"label": "Feature Idea Cloud",
|
||||||
|
"description": "Brainstorm and organize feature ideas",
|
||||||
|
"icon": "Lightbulb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "enabledCount",
|
||||||
|
"type": "static",
|
||||||
|
"expression": "Object.values(data.features || {}).filter(Boolean).length"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "totalCount",
|
||||||
|
"type": "static",
|
||||||
|
"expression": "Object.keys(data.features || {}).length"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "root",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "h-full p-6 bg-background"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "header",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "mb-6"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "title",
|
||||||
|
"type": "Heading",
|
||||||
|
"props": {
|
||||||
|
"level": 2,
|
||||||
|
"className": "text-2xl font-bold mb-2",
|
||||||
|
"children": "Feature Toggles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "description",
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"className": "text-muted-foreground"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"value": "Enable or disable features to customize your workspace. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"dataBinding": "enabledCount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"value": " of "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"dataBinding": "totalCount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"value": " features enabled."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scroll-area",
|
||||||
|
"type": "ScrollArea",
|
||||||
|
"props": {
|
||||||
|
"className": "h-[calc(100vh-200px)]"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "grid",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "grid grid-cols-1 lg:grid-cols-2 gap-4 pr-4"
|
||||||
|
},
|
||||||
|
"loop": {
|
||||||
|
"source": "featuresList",
|
||||||
|
"itemVar": "item",
|
||||||
|
"indexVar": "index"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "feature-card",
|
||||||
|
"type": "Card",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "card-header",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "p-6 pb-3"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "card-content",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex items-start justify-between"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "left-content",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex items-center gap-3"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "icon-container",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": {
|
||||||
|
"expression": "data.features?.[item.key] ? 'p-2 rounded-lg bg-primary text-primary-foreground' : 'p-2 rounded-lg bg-muted text-muted-foreground'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "icon",
|
||||||
|
"type": {
|
||||||
|
"dataBinding": "item.icon"
|
||||||
|
},
|
||||||
|
"props": {
|
||||||
|
"size": 20,
|
||||||
|
"weight": "duotone"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-content",
|
||||||
|
"type": "div",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "title",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-base font-semibold"
|
||||||
|
},
|
||||||
|
"dataBinding": "item.label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "description",
|
||||||
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "text-xs mt-1 text-muted-foreground"
|
||||||
|
},
|
||||||
|
"dataBinding": "item.description"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "switch",
|
||||||
|
"type": "Switch",
|
||||||
|
"bindings": {
|
||||||
|
"checked": {
|
||||||
|
"expression": "data.features?.[item.key] || false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"event": "checkedChange",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "updateFeature",
|
||||||
|
"type": "custom",
|
||||||
|
"params": {
|
||||||
|
"key": "item.key",
|
||||||
|
"checked": "event"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"valueTemplate": {
|
"valueTemplate": {
|
||||||
"total": "data.todos.length",
|
"total": "data.todos.length",
|
||||||
"completed": "data.todos.filter(completed === true).length",
|
"completed": "data.todos.filter(completed === true).length",
|
||||||
|
|||||||
48
src/store/actionNames.ts
Normal file
48
src/store/actionNames.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const itemSlices = [
|
||||||
|
'files',
|
||||||
|
'models',
|
||||||
|
'components',
|
||||||
|
'componentTrees',
|
||||||
|
'workflows',
|
||||||
|
'lambdas',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const itemChangeActionNames = ['addItem', 'updateItem', 'removeItem'] as const
|
||||||
|
|
||||||
|
export const itemChangeActionTypes = new Set(
|
||||||
|
itemSlices.flatMap((slice) =>
|
||||||
|
itemChangeActionNames.map((actionName) => `${slice}/${actionName}`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const persistenceSingleItemActionNames = new Set([
|
||||||
|
'addItem',
|
||||||
|
'updateItem',
|
||||||
|
'saveFile',
|
||||||
|
'saveModel',
|
||||||
|
'saveComponent',
|
||||||
|
'saveComponentTree',
|
||||||
|
'saveWorkflow',
|
||||||
|
'saveLambda',
|
||||||
|
])
|
||||||
|
|
||||||
|
export const persistenceBulkActionNames = new Set([
|
||||||
|
'addItems',
|
||||||
|
'setItems',
|
||||||
|
'setFiles',
|
||||||
|
'setModels',
|
||||||
|
'setComponents',
|
||||||
|
'setComponentTrees',
|
||||||
|
'setWorkflows',
|
||||||
|
'setLambdas',
|
||||||
|
])
|
||||||
|
|
||||||
|
export const persistenceDeleteActionNames = new Set([
|
||||||
|
'removeItem',
|
||||||
|
'deleteFile',
|
||||||
|
'deleteModel',
|
||||||
|
'deleteComponent',
|
||||||
|
'deleteComponentTree',
|
||||||
|
'deleteWorkflow',
|
||||||
|
'deleteLambda',
|
||||||
|
])
|
||||||
112
src/store/middleware/autoSyncMiddleware.test.ts
Normal file
112
src/store/middleware/autoSyncMiddleware.test.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { AutoSyncManager } from './autoSyncMiddleware'
|
||||||
|
|
||||||
|
const { syncToFlaskBulkMock } = vi.hoisted(() => ({
|
||||||
|
syncToFlaskBulkMock: vi.fn(() => ({ type: 'sync/bulk' })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../slices/syncSlice', () => ({
|
||||||
|
syncToFlaskBulk: syncToFlaskBulkMock,
|
||||||
|
checkFlaskConnection: vi.fn(() => ({ type: 'sync/check' })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
|
const waitFor = async (assertion: () => void, attempts = 5) => {
|
||||||
|
let lastError: unknown
|
||||||
|
|
||||||
|
for (let i = 0; i < attempts; i += 1) {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertion()
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
const createControlledPromise = () => {
|
||||||
|
let resolve: () => void
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolvePromise) => {
|
||||||
|
resolve = resolvePromise
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise,
|
||||||
|
resolve: resolve!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AutoSyncManager', () => {
|
||||||
|
let manager: AutoSyncManager
|
||||||
|
let dispatchMock: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new AutoSyncManager()
|
||||||
|
dispatchMock = vi.fn()
|
||||||
|
manager.setDispatch(dispatchMock)
|
||||||
|
syncToFlaskBulkMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('serializes performSync calls', async () => {
|
||||||
|
const firstSync = createControlledPromise()
|
||||||
|
dispatchMock
|
||||||
|
.mockReturnValueOnce(firstSync.promise)
|
||||||
|
.mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
|
const firstRun = manager.syncNow()
|
||||||
|
const secondRun = manager.syncNow()
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(dispatchMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
firstSync.resolve()
|
||||||
|
await Promise.all([firstRun, secondRun])
|
||||||
|
|
||||||
|
expect(dispatchMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets changeCounter after a successful sync', async () => {
|
||||||
|
dispatchMock.mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
manager.trackChange()
|
||||||
|
manager.trackChange()
|
||||||
|
|
||||||
|
await manager.syncNow()
|
||||||
|
|
||||||
|
expect(manager.getStatus().changeCounter).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('runs one pending sync after an in-flight sync finishes', async () => {
|
||||||
|
const firstSync = createControlledPromise()
|
||||||
|
dispatchMock
|
||||||
|
.mockReturnValueOnce(firstSync.promise)
|
||||||
|
.mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
|
const syncPromise = manager.syncNow()
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(dispatchMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
manager.trackChange()
|
||||||
|
manager.trackChange()
|
||||||
|
|
||||||
|
firstSync.resolve()
|
||||||
|
await syncPromise
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(dispatchMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Middleware } from '@reduxjs/toolkit'
|
import { Middleware } from '@reduxjs/toolkit'
|
||||||
import { syncToFlaskBulk, checkFlaskConnection } from '../slices/syncSlice'
|
import { syncToFlaskBulk, checkFlaskConnection } from '../slices/syncSlice'
|
||||||
import { RootState } from '../index'
|
import { RootState } from '../index'
|
||||||
|
import { itemChangeActionTypes } from '../actionNames'
|
||||||
|
|
||||||
interface AutoSyncConfig {
|
interface AutoSyncConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -9,7 +10,7 @@ interface AutoSyncConfig {
|
|||||||
maxQueueSize: number
|
maxQueueSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoSyncManager {
|
export class AutoSyncManager {
|
||||||
private config: AutoSyncConfig = {
|
private config: AutoSyncConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
intervalMs: 30000,
|
intervalMs: 30000,
|
||||||
@@ -20,6 +21,8 @@ class AutoSyncManager {
|
|||||||
private timer: ReturnType<typeof setTimeout> | null = null
|
private timer: ReturnType<typeof setTimeout> | null = null
|
||||||
private lastSyncTime = 0
|
private lastSyncTime = 0
|
||||||
private changeCounter = 0
|
private changeCounter = 0
|
||||||
|
private inFlight = false
|
||||||
|
private pendingSync = false
|
||||||
private dispatch: any = null
|
private dispatch: any = null
|
||||||
|
|
||||||
configure(config: Partial<AutoSyncConfig>) {
|
configure(config: Partial<AutoSyncConfig>) {
|
||||||
@@ -68,18 +71,33 @@ class AutoSyncManager {
|
|||||||
|
|
||||||
private async performSync() {
|
private async performSync() {
|
||||||
if (!this.dispatch) return
|
if (!this.dispatch) return
|
||||||
|
if (this.inFlight) {
|
||||||
|
this.pendingSync = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inFlight = true
|
||||||
try {
|
try {
|
||||||
await this.dispatch(syncToFlaskBulk())
|
await this.dispatch(syncToFlaskBulk())
|
||||||
this.lastSyncTime = Date.now()
|
this.lastSyncTime = Date.now()
|
||||||
this.changeCounter = 0
|
this.changeCounter = 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AutoSync] Sync failed:', error)
|
console.error('[AutoSync] Sync failed:', error)
|
||||||
|
} finally {
|
||||||
|
this.inFlight = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingSync) {
|
||||||
|
this.pendingSync = false
|
||||||
|
await this.performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackChange() {
|
trackChange() {
|
||||||
this.changeCounter++
|
this.changeCounter++
|
||||||
|
if (this.inFlight) {
|
||||||
|
this.pendingSync = true
|
||||||
|
}
|
||||||
|
|
||||||
if (this.changeCounter >= this.config.maxQueueSize && this.config.syncOnChange) {
|
if (this.changeCounter >= this.config.maxQueueSize && this.config.syncOnChange) {
|
||||||
this.performSync()
|
this.performSync()
|
||||||
@@ -127,28 +145,7 @@ export const createAutoSyncMiddleware = (): Middleware => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeActions = [
|
if (itemChangeActionTypes.has(action.type)) {
|
||||||
'files/addItem',
|
|
||||||
'files/updateItem',
|
|
||||||
'files/removeItem',
|
|
||||||
'models/addItem',
|
|
||||||
'models/updateItem',
|
|
||||||
'models/removeItem',
|
|
||||||
'components/addItem',
|
|
||||||
'components/updateItem',
|
|
||||||
'components/removeItem',
|
|
||||||
'componentTrees/addItem',
|
|
||||||
'componentTrees/updateItem',
|
|
||||||
'componentTrees/removeItem',
|
|
||||||
'workflows/addItem',
|
|
||||||
'workflows/updateItem',
|
|
||||||
'workflows/removeItem',
|
|
||||||
'lambdas/addItem',
|
|
||||||
'lambdas/updateItem',
|
|
||||||
'lambdas/removeItem',
|
|
||||||
]
|
|
||||||
|
|
||||||
if (changeActions.includes(action.type)) {
|
|
||||||
autoSyncManager.trackChange()
|
autoSyncManager.trackChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export async function syncToFlask(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FlaskSync] Error syncing to Flask:', error)
|
console.error('[FlaskSync] Error syncing to Flask:', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
src/store/middleware/persistenceMiddleware.test.ts
Normal file
103
src/store/middleware/persistenceMiddleware.test.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { PersistenceQueue } from './persistenceMiddleware'
|
||||||
|
|
||||||
|
const { putMock, deleteMock, syncMock } = vi.hoisted(() => ({
|
||||||
|
putMock: vi.fn<[string, unknown], Promise<void>>(),
|
||||||
|
deleteMock: vi.fn<[string, string], Promise<void>>(),
|
||||||
|
syncMock: vi.fn<[string, string, unknown, string], Promise<void>>()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/lib/db', () => ({
|
||||||
|
db: {
|
||||||
|
put: putMock,
|
||||||
|
delete: deleteMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('./flaskSync', () => ({
|
||||||
|
syncToFlask: syncMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
|
const waitFor = async (assertion: () => void, attempts = 5) => {
|
||||||
|
let lastError: unknown
|
||||||
|
|
||||||
|
for (let i = 0; i < attempts; i += 1) {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertion()
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
const createControlledPromise = () => {
|
||||||
|
let resolve: () => void
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolvePromise) => {
|
||||||
|
resolve = resolvePromise
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise,
|
||||||
|
resolve: resolve!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PersistenceQueue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
putMock.mockReset()
|
||||||
|
deleteMock.mockReset()
|
||||||
|
syncMock.mockReset()
|
||||||
|
syncMock.mockResolvedValue(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flushes new operations enqueued while processing after the first batch finishes', async () => {
|
||||||
|
const queue = new PersistenceQueue()
|
||||||
|
const controlled = createControlledPromise()
|
||||||
|
|
||||||
|
putMock
|
||||||
|
.mockReturnValueOnce(controlled.promise)
|
||||||
|
.mockResolvedValueOnce(undefined)
|
||||||
|
|
||||||
|
queue.enqueue({
|
||||||
|
type: 'put',
|
||||||
|
storeName: 'files',
|
||||||
|
key: 'file-1',
|
||||||
|
value: { id: 'file-1' },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(putMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
queue.enqueue({
|
||||||
|
type: 'put',
|
||||||
|
storeName: 'files',
|
||||||
|
key: 'file-2',
|
||||||
|
value: { id: 'file-2' },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(putMock).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
controlled.resolve()
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(putMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,6 +2,11 @@ import { Middleware } from '@reduxjs/toolkit'
|
|||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { syncToFlask } from './flaskSync'
|
import { syncToFlask } from './flaskSync'
|
||||||
import { RootState } from '../index'
|
import { RootState } from '../index'
|
||||||
|
import {
|
||||||
|
persistenceBulkActionNames,
|
||||||
|
persistenceDeleteActionNames,
|
||||||
|
persistenceSingleItemActionNames,
|
||||||
|
} from '../actionNames'
|
||||||
|
|
||||||
interface PersistenceConfig {
|
interface PersistenceConfig {
|
||||||
storeName: string
|
storeName: string
|
||||||
@@ -38,10 +43,23 @@ type PendingOperation = {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FailedSyncOperation = PendingOperation & {
|
||||||
|
attempt: number
|
||||||
|
lastError: string
|
||||||
|
nextRetryAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SYNC_RETRIES = 5
|
||||||
|
const BASE_SYNC_RETRY_DELAY_MS = 1000
|
||||||
|
const MAX_SYNC_RETRY_DELAY_MS = 30000
|
||||||
|
|
||||||
class PersistenceQueue {
|
class PersistenceQueue {
|
||||||
private queue: Map<string, PendingOperation> = new Map()
|
private queue: Map<string, PendingOperation> = new Map()
|
||||||
private processing = false
|
private processing = false
|
||||||
|
private pendingFlush = false
|
||||||
private debounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
private debounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||||
|
private failedSyncs: Map<string, FailedSyncOperation> = new Map()
|
||||||
|
private retryTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||||
|
|
||||||
enqueue(operation: PendingOperation, debounceMs: number) {
|
enqueue(operation: PendingOperation, debounceMs: number) {
|
||||||
const opKey = `${operation.storeName}:${operation.key}`
|
const opKey = `${operation.storeName}:${operation.key}`
|
||||||
@@ -62,7 +80,12 @@ class PersistenceQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processQueue() {
|
async processQueue() {
|
||||||
if (this.processing || this.queue.size === 0) return
|
if (this.processing) {
|
||||||
|
this.pendingFlush = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.queue.size === 0) return
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
@@ -75,14 +98,10 @@ class PersistenceQueue {
|
|||||||
try {
|
try {
|
||||||
if (op.type === 'put') {
|
if (op.type === 'put') {
|
||||||
await db.put(op.storeName as any, op.value)
|
await db.put(op.storeName as any, op.value)
|
||||||
if (sliceToPersistenceMap[op.storeName]?.syncToFlask) {
|
await this.syncToFlaskWithRetry(op, op.value)
|
||||||
await syncToFlask(op.storeName, op.key, op.value, 'put')
|
|
||||||
}
|
|
||||||
} else if (op.type === 'delete') {
|
} else if (op.type === 'delete') {
|
||||||
await db.delete(op.storeName as any, op.key)
|
await db.delete(op.storeName as any, op.key)
|
||||||
if (sliceToPersistenceMap[op.storeName]?.syncToFlask) {
|
await this.syncToFlaskWithRetry(op, null)
|
||||||
await syncToFlask(op.storeName, op.key, null, 'delete')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[PersistenceMiddleware] Failed to persist ${op.type} for ${op.storeName}:${op.key}`, error)
|
console.error(`[PersistenceMiddleware] Failed to persist ${op.type} for ${op.storeName}:${op.key}`, error)
|
||||||
@@ -97,6 +116,23 @@ class PersistenceQueue {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
const needsFlush = this.pendingFlush || this.queue.size > 0
|
||||||
|
this.pendingFlush = false
|
||||||
|
if (needsFlush) {
|
||||||
|
await this.processQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFailedSyncs() {
|
||||||
|
return Array.from(this.failedSyncs.values()).sort((a, b) => a.nextRetryAt - b.nextRetryAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryFailedSyncs() {
|
||||||
|
for (const [opKey, failure] of this.failedSyncs.entries()) {
|
||||||
|
if (failure.nextRetryAt <= Date.now()) {
|
||||||
|
await this.retryFailedSync(opKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +143,89 @@ class PersistenceQueue {
|
|||||||
this.debounceTimers.clear()
|
this.debounceTimers.clear()
|
||||||
await this.processQueue()
|
await this.processQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async syncToFlaskWithRetry(op: PendingOperation, value: any) {
|
||||||
|
if (!sliceToPersistenceMap[op.storeName]?.syncToFlask) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await syncToFlask(op.storeName, op.key, value, op.type)
|
||||||
|
this.clearSyncFailure(op)
|
||||||
|
} catch (error) {
|
||||||
|
this.recordSyncFailure(op, error)
|
||||||
|
console.warn(
|
||||||
|
`[PersistenceMiddleware] Flask sync failed for ${op.storeName}:${op.key} (${op.type}); queued for retry.`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordSyncFailure(op: PendingOperation, error: unknown) {
|
||||||
|
const opKey = this.getFailureKey(op)
|
||||||
|
const previous = this.failedSyncs.get(opKey)
|
||||||
|
const attempt = previous ? previous.attempt + 1 : 1
|
||||||
|
const delayMs = this.getRetryDelayMs(attempt)
|
||||||
|
const nextRetryAt = Date.now() + delayMs
|
||||||
|
const lastError = error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
this.failedSyncs.set(opKey, {
|
||||||
|
...op,
|
||||||
|
attempt,
|
||||||
|
lastError,
|
||||||
|
nextRetryAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingTimer = this.retryTimers.get(opKey)
|
||||||
|
if (existingTimer) {
|
||||||
|
clearTimeout(existingTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt <= MAX_SYNC_RETRIES) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.retryTimers.delete(opKey)
|
||||||
|
void this.retryFailedSync(opKey)
|
||||||
|
}, delayMs)
|
||||||
|
this.retryTimers.set(opKey, timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSyncFailure(op: PendingOperation) {
|
||||||
|
const opKey = this.getFailureKey(op)
|
||||||
|
const timer = this.retryTimers.get(opKey)
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
this.retryTimers.delete(opKey)
|
||||||
|
}
|
||||||
|
this.failedSyncs.delete(opKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retryFailedSync(opKey: string) {
|
||||||
|
const failure = this.failedSyncs.get(opKey)
|
||||||
|
if (!failure) return
|
||||||
|
|
||||||
|
if (failure.attempt > MAX_SYNC_RETRIES) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enqueue(
|
||||||
|
{
|
||||||
|
type: failure.type,
|
||||||
|
storeName: failure.storeName,
|
||||||
|
key: failure.key,
|
||||||
|
value: failure.value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRetryDelayMs(attempt: number) {
|
||||||
|
const delay = BASE_SYNC_RETRY_DELAY_MS * Math.pow(2, attempt - 1)
|
||||||
|
return Math.min(delay, MAX_SYNC_RETRY_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFailureKey(op: PendingOperation) {
|
||||||
|
return `${op.storeName}:${op.key}:${op.type}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const persistenceQueue = new PersistenceQueue()
|
const persistenceQueue = new PersistenceQueue()
|
||||||
@@ -128,10 +247,7 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
if (!sliceState) return result
|
if (!sliceState) return result
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (actionName === 'addItem' || actionName === 'updateItem' || actionName === 'saveFile' ||
|
if (persistenceSingleItemActionNames.has(actionName)) {
|
||||||
actionName === 'saveModel' || actionName === 'saveComponent' || actionName === 'saveComponentTree' ||
|
|
||||||
actionName === 'saveWorkflow' || actionName === 'saveLambda') {
|
|
||||||
|
|
||||||
const item = action.payload
|
const item = action.payload
|
||||||
if (item && item.id) {
|
if (item && item.id) {
|
||||||
persistenceQueue.enqueue({
|
persistenceQueue.enqueue({
|
||||||
@@ -144,10 +260,7 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionName === 'addItems' || actionName === 'setItems' || actionName === 'setFiles' ||
|
if (persistenceBulkActionNames.has(actionName)) {
|
||||||
actionName === 'setModels' || actionName === 'setComponents' || actionName === 'setComponentTrees' ||
|
|
||||||
actionName === 'setWorkflows' || actionName === 'setLambdas') {
|
|
||||||
|
|
||||||
const items = action.payload
|
const items = action.payload
|
||||||
if (Array.isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
items.forEach((item: any) => {
|
items.forEach((item: any) => {
|
||||||
@@ -164,10 +277,7 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionName === 'removeItem' || actionName === 'deleteFile' || actionName === 'deleteModel' ||
|
if (persistenceDeleteActionNames.has(actionName)) {
|
||||||
actionName === 'deleteComponent' || actionName === 'deleteComponentTree' ||
|
|
||||||
actionName === 'deleteWorkflow' || actionName === 'deleteLambda') {
|
|
||||||
|
|
||||||
const itemId = typeof action.payload === 'string' ? action.payload : action.payload?.id
|
const itemId = typeof action.payload === 'string' ? action.payload : action.payload?.id
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
persistenceQueue.enqueue({
|
persistenceQueue.enqueue({
|
||||||
@@ -208,6 +318,8 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const flushPersistence = () => persistenceQueue.flush()
|
export const flushPersistence = () => persistenceQueue.flush()
|
||||||
|
export const getFailedSyncOperations = () => persistenceQueue.getFailedSyncs()
|
||||||
|
export const retryFailedSyncOperations = () => persistenceQueue.retryFailedSyncs()
|
||||||
|
|
||||||
export const configurePersistence = (sliceName: string, config: Partial<PersistenceConfig>) => {
|
export const configurePersistence = (sliceName: string, config: Partial<PersistenceConfig>) => {
|
||||||
if (sliceToPersistenceMap[sliceName]) {
|
if (sliceToPersistenceMap[sliceName]) {
|
||||||
|
|||||||
@@ -107,21 +107,18 @@ export const createSyncMonitorMiddleware = (): Middleware => {
|
|||||||
const isFulfilledAction = asyncThunkActions.some((prefix) => action.type === `${prefix}/fulfilled`)
|
const isFulfilledAction = asyncThunkActions.some((prefix) => action.type === `${prefix}/fulfilled`)
|
||||||
const isRejectedAction = asyncThunkActions.some((prefix) => action.type === `${prefix}/rejected`)
|
const isRejectedAction = asyncThunkActions.some((prefix) => action.type === `${prefix}/rejected`)
|
||||||
|
|
||||||
if (isPendingAction) {
|
if (isPendingAction && action.meta?.requestId) {
|
||||||
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
syncMonitor.startOperation(action.meta.requestId)
|
||||||
syncMonitor.startOperation(operationId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = next(action)
|
const result = next(action)
|
||||||
|
|
||||||
if (isFulfilledAction) {
|
if (isFulfilledAction && action.meta?.requestId) {
|
||||||
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
syncMonitor.endOperation(action.meta.requestId, true)
|
||||||
syncMonitor.endOperation(operationId, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRejectedAction) {
|
if (isRejectedAction && action.meta?.requestId) {
|
||||||
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
syncMonitor.endOperation(action.meta.requestId, false)
|
||||||
syncMonitor.endOperation(operationId, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
98
src/store/slices/syncSlice.test.ts
Normal file
98
src/store/slices/syncSlice.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockFetchAllFromFlask,
|
||||||
|
mockDbPut,
|
||||||
|
mockDbGetAll,
|
||||||
|
mockDbDelete
|
||||||
|
} = vi.hoisted(() => {
|
||||||
|
return {
|
||||||
|
mockFetchAllFromFlask: vi.fn<[], Promise<Record<string, any>>>(),
|
||||||
|
mockDbPut: vi.fn<[string, any], Promise<void>>(),
|
||||||
|
mockDbGetAll: vi.fn<[string], Promise<any[]>>(),
|
||||||
|
mockDbDelete: vi.fn<[string, string], Promise<void>>()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/store/middleware/flaskSync', () => ({
|
||||||
|
fetchAllFromFlask: mockFetchAllFromFlask
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/lib/db', () => ({
|
||||||
|
db: {
|
||||||
|
put: mockDbPut,
|
||||||
|
getAll: mockDbGetAll,
|
||||||
|
delete: mockDbDelete
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { syncFromFlaskBulk } from './syncSlice'
|
||||||
|
|
||||||
|
describe('syncFromFlaskBulk', () => {
|
||||||
|
const dispatch = vi.fn()
|
||||||
|
const getState = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFetchAllFromFlask.mockReset()
|
||||||
|
mockDbPut.mockReset()
|
||||||
|
mockDbGetAll.mockReset()
|
||||||
|
mockDbDelete.mockReset()
|
||||||
|
dispatch.mockReset()
|
||||||
|
getState.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores invalid keys from Flask', async () => {
|
||||||
|
mockFetchAllFromFlask.mockResolvedValue({
|
||||||
|
'unknown:1': { id: '1' },
|
||||||
|
'files': { id: 'missing-colon' },
|
||||||
|
'models:': { id: 'empty-id' },
|
||||||
|
'components:abc:extra': { id: 'abc' }
|
||||||
|
})
|
||||||
|
mockDbGetAll.mockResolvedValue([])
|
||||||
|
|
||||||
|
const action = await syncFromFlaskBulk()(dispatch, getState, undefined)
|
||||||
|
|
||||||
|
expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled')
|
||||||
|
expect(mockDbPut).not.toHaveBeenCalled()
|
||||||
|
expect(mockDbDelete).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates local DB for valid keys', async () => {
|
||||||
|
const file = { id: 'file-1', name: 'File 1' }
|
||||||
|
const model = { id: 'model-1', name: 'Model 1' }
|
||||||
|
|
||||||
|
mockFetchAllFromFlask.mockResolvedValue({
|
||||||
|
'files:file-1': file,
|
||||||
|
'models:model-1': model
|
||||||
|
})
|
||||||
|
mockDbGetAll.mockResolvedValue([])
|
||||||
|
|
||||||
|
const action = await syncFromFlaskBulk()(dispatch, getState, undefined)
|
||||||
|
|
||||||
|
expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled')
|
||||||
|
expect(mockDbPut).toHaveBeenCalledWith('files', file)
|
||||||
|
expect(mockDbPut).toHaveBeenCalledWith('models', model)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes local entries missing from Flask data', async () => {
|
||||||
|
const file = { id: 'keep', name: 'Keep' }
|
||||||
|
|
||||||
|
mockFetchAllFromFlask.mockResolvedValue({
|
||||||
|
'files:keep': file
|
||||||
|
})
|
||||||
|
mockDbGetAll.mockImplementation((storeName) => {
|
||||||
|
if (storeName === 'files') {
|
||||||
|
return Promise.resolve([file, { id: 'stale', name: 'Stale' }])
|
||||||
|
}
|
||||||
|
return Promise.resolve([])
|
||||||
|
})
|
||||||
|
|
||||||
|
const action = await syncFromFlaskBulk()(dispatch, getState, undefined)
|
||||||
|
|
||||||
|
expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled')
|
||||||
|
expect(mockDbPut).toHaveBeenCalledWith('files', file)
|
||||||
|
expect(mockDbDelete).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockDbDelete).toHaveBeenCalledWith('files', 'stale')
|
||||||
|
expect(mockDbDelete).not.toHaveBeenCalledWith('files', 'keep')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,6 +9,8 @@ import { db } from '@/lib/db'
|
|||||||
|
|
||||||
export type SyncStatus = 'idle' | 'syncing' | 'success' | 'error'
|
export type SyncStatus = 'idle' | 'syncing' | 'success' | 'error'
|
||||||
|
|
||||||
|
const SYNCABLE_STORES = new Set(['files', 'models', 'components', 'workflows'])
|
||||||
|
|
||||||
interface SyncState {
|
interface SyncState {
|
||||||
status: SyncStatus
|
status: SyncStatus
|
||||||
lastSyncedAt: number | null
|
lastSyncedAt: number | null
|
||||||
@@ -68,15 +70,51 @@ export const syncFromFlaskBulk = createAsyncThunk(
|
|||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchAllFromFlask()
|
const data = await fetchAllFromFlask()
|
||||||
|
const allowedStoreNames = new Set(['files', 'models', 'components', 'workflows'])
|
||||||
|
const serverIdsByStore = {
|
||||||
|
files: new Set<string>(),
|
||||||
|
models: new Set<string>(),
|
||||||
|
components: new Set<string>(),
|
||||||
|
workflows: new Set<string>(),
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
const [storeName, id] = key.split(':')
|
const [storeName, id] = key.split(':')
|
||||||
|
|
||||||
if (storeName === 'files' ||
|
if (SYNCABLE_STORES.has(storeName)) {
|
||||||
storeName === 'models' ||
|
|
||||||
storeName === 'components' ||
|
|
||||||
storeName === 'workflows') {
|
|
||||||
await db.put(storeName as any, value)
|
await db.put(storeName as any, value)
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = key.split(':')
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const [storeName, id] = parts
|
||||||
|
if (!storeName || !id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowedStoreNames.has(storeName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serverIdsByStore[storeName as keyof typeof serverIdsByStore].add(id)
|
||||||
|
await db.put(storeName as any, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit merge strategy: server is source of truth; delete local records missing from server response.
|
||||||
|
const storeNames = Array.from(allowedStoreNames)
|
||||||
|
for (const storeName of storeNames) {
|
||||||
|
const localRecords = await db.getAll(storeName as any)
|
||||||
|
for (const record of localRecords) {
|
||||||
|
const recordId = record?.id
|
||||||
|
const recordIdString = recordId == null ? '' : String(recordId)
|
||||||
|
if (!serverIdsByStore[storeName as keyof typeof serverIdsByStore].has(recordIdString)) {
|
||||||
|
await db.delete(storeName as any, recordId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
249
src/types/json-ui-component-types.ts
Normal file
249
src/types/json-ui-component-types.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
// This file is auto-generated by scripts/generate-json-ui-component-types.ts.
|
||||||
|
// Do not edit this file directly.
|
||||||
|
|
||||||
|
export const jsonUIComponentTypes = [
|
||||||
|
"div",
|
||||||
|
"section",
|
||||||
|
"article",
|
||||||
|
"header",
|
||||||
|
"footer",
|
||||||
|
"main",
|
||||||
|
"ActionCard",
|
||||||
|
"AlertDialog",
|
||||||
|
"Card",
|
||||||
|
"CodeExplanationDialog",
|
||||||
|
"CompletionCard",
|
||||||
|
"ComponentBindingDialog",
|
||||||
|
"ComponentBindingDialogWrapper",
|
||||||
|
"Container",
|
||||||
|
"DataSourceCard",
|
||||||
|
"DataSourceEditorDialog",
|
||||||
|
"DataSourceEditorDialogWrapper",
|
||||||
|
"Dialog",
|
||||||
|
"Drawer",
|
||||||
|
"Flex",
|
||||||
|
"GlowCard",
|
||||||
|
"Grid",
|
||||||
|
"HoverCard",
|
||||||
|
"Modal",
|
||||||
|
"ResponsiveGrid",
|
||||||
|
"Section",
|
||||||
|
"Stack",
|
||||||
|
"TipsCard",
|
||||||
|
"TreeCard",
|
||||||
|
"TreeFormDialog",
|
||||||
|
"ActionButton",
|
||||||
|
"Button",
|
||||||
|
"ButtonGroup",
|
||||||
|
"Checkbox",
|
||||||
|
"ConfirmButton",
|
||||||
|
"CopyButton",
|
||||||
|
"DatePicker",
|
||||||
|
"FileUpload",
|
||||||
|
"FilterInput",
|
||||||
|
"Form",
|
||||||
|
"IconButton",
|
||||||
|
"Input",
|
||||||
|
"InputOtp",
|
||||||
|
"NumberInput",
|
||||||
|
"PasswordInput",
|
||||||
|
"QuickActionButton",
|
||||||
|
"Radio",
|
||||||
|
"RadioGroup",
|
||||||
|
"RangeSlider",
|
||||||
|
"Select",
|
||||||
|
"Slider",
|
||||||
|
"Switch",
|
||||||
|
"TextArea",
|
||||||
|
"Toggle",
|
||||||
|
"ToggleGroup",
|
||||||
|
"ToolbarButton",
|
||||||
|
"ActionIcon",
|
||||||
|
"Avatar",
|
||||||
|
"AvatarGroup",
|
||||||
|
"Badge",
|
||||||
|
"CircularProgress",
|
||||||
|
"Code",
|
||||||
|
"Divider",
|
||||||
|
"FileIcon",
|
||||||
|
"Heading",
|
||||||
|
"HelperText",
|
||||||
|
"IconText",
|
||||||
|
"IconWrapper",
|
||||||
|
"Image",
|
||||||
|
"Label",
|
||||||
|
"Progress",
|
||||||
|
"ProgressBar",
|
||||||
|
"SchemaCodeViewer",
|
||||||
|
"Separator",
|
||||||
|
"Skeleton",
|
||||||
|
"Spinner",
|
||||||
|
"Tag",
|
||||||
|
"Text",
|
||||||
|
"Textarea",
|
||||||
|
"TextGradient",
|
||||||
|
"TextHighlight",
|
||||||
|
"TreeIcon",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"Check",
|
||||||
|
"X",
|
||||||
|
"Plus",
|
||||||
|
"Minus",
|
||||||
|
"Search",
|
||||||
|
"Filter",
|
||||||
|
"Download",
|
||||||
|
"Upload",
|
||||||
|
"Edit",
|
||||||
|
"Trash",
|
||||||
|
"Eye",
|
||||||
|
"EyeOff",
|
||||||
|
"ChevronUp",
|
||||||
|
"ChevronDown",
|
||||||
|
"ChevronLeft",
|
||||||
|
"ChevronRight",
|
||||||
|
"Settings",
|
||||||
|
"User",
|
||||||
|
"Bell",
|
||||||
|
"Mail",
|
||||||
|
"Calendar",
|
||||||
|
"Clock",
|
||||||
|
"Star",
|
||||||
|
"Heart",
|
||||||
|
"Share",
|
||||||
|
"Link",
|
||||||
|
"Copy",
|
||||||
|
"Save",
|
||||||
|
"RefreshCw",
|
||||||
|
"AlertCircle",
|
||||||
|
"Info",
|
||||||
|
"HelpCircle",
|
||||||
|
"Home",
|
||||||
|
"Menu",
|
||||||
|
"MoreVertical",
|
||||||
|
"MoreHorizontal",
|
||||||
|
"Breadcrumb",
|
||||||
|
"ContextMenu",
|
||||||
|
"DropdownMenu",
|
||||||
|
"FileTabs",
|
||||||
|
"Menubar",
|
||||||
|
"NavigationGroupHeader",
|
||||||
|
"NavigationItem",
|
||||||
|
"NavigationMenu",
|
||||||
|
"TabIcon",
|
||||||
|
"Tabs",
|
||||||
|
"Alert",
|
||||||
|
"CountBadge",
|
||||||
|
"DataSourceBadge",
|
||||||
|
"EmptyCanvasState",
|
||||||
|
"EmptyEditorState",
|
||||||
|
"EmptyMessage",
|
||||||
|
"EmptyState",
|
||||||
|
"EmptyStateIcon",
|
||||||
|
"ErrorBadge",
|
||||||
|
"GitHubBuildStatus",
|
||||||
|
"GitHubBuildStatusWrapper",
|
||||||
|
"InfoBox",
|
||||||
|
"LabelWithBadge",
|
||||||
|
"LoadingFallback",
|
||||||
|
"LoadingSpinner",
|
||||||
|
"LoadingState",
|
||||||
|
"Notification",
|
||||||
|
"SchemaEditorStatusBar",
|
||||||
|
"SeedDataStatus",
|
||||||
|
"StatusBadge",
|
||||||
|
"StatusIcon",
|
||||||
|
"Chart",
|
||||||
|
"DataList",
|
||||||
|
"DataSourceManager",
|
||||||
|
"DataTable",
|
||||||
|
"KeyValue",
|
||||||
|
"LazyBarChart",
|
||||||
|
"LazyBarChartWrapper",
|
||||||
|
"LazyD3BarChart",
|
||||||
|
"LazyD3BarChartWrapper",
|
||||||
|
"LazyLineChart",
|
||||||
|
"LazyLineChartWrapper",
|
||||||
|
"List",
|
||||||
|
"ListItem",
|
||||||
|
"MetricCard",
|
||||||
|
"MetricDisplay",
|
||||||
|
"SeedDataManager",
|
||||||
|
"SeedDataManagerWrapper",
|
||||||
|
"StatCard",
|
||||||
|
"Table",
|
||||||
|
"TableHeader",
|
||||||
|
"TableBody",
|
||||||
|
"TableRow",
|
||||||
|
"TableCell",
|
||||||
|
"TableHead",
|
||||||
|
"Timeline",
|
||||||
|
"TreeListHeader",
|
||||||
|
"TreeListPanel",
|
||||||
|
"Accordion",
|
||||||
|
"ActionBar",
|
||||||
|
"AppBranding",
|
||||||
|
"AppHeader",
|
||||||
|
"AppLogo",
|
||||||
|
"AspectRatio",
|
||||||
|
"BindingEditor",
|
||||||
|
"BindingIndicator",
|
||||||
|
"CanvasRenderer",
|
||||||
|
"Carousel",
|
||||||
|
"Chip",
|
||||||
|
"Collapsible",
|
||||||
|
"ColorSwatch",
|
||||||
|
"Command",
|
||||||
|
"CommandPalette",
|
||||||
|
"ComponentPalette",
|
||||||
|
"ComponentPaletteItem",
|
||||||
|
"ComponentTree",
|
||||||
|
"ComponentTreeWrapper",
|
||||||
|
"ComponentTreeNode",
|
||||||
|
"DataCard",
|
||||||
|
"DetailRow",
|
||||||
|
"Dot",
|
||||||
|
"EditorActions",
|
||||||
|
"EditorToolbar",
|
||||||
|
"InfoPanel",
|
||||||
|
"JSONUIShowcase",
|
||||||
|
"Kbd",
|
||||||
|
"LazyInlineMonacoEditor",
|
||||||
|
"LazyMonacoEditor",
|
||||||
|
"LiveIndicator",
|
||||||
|
"MonacoEditorPanel",
|
||||||
|
"PageHeader",
|
||||||
|
"PageHeaderContent",
|
||||||
|
"Pagination",
|
||||||
|
"PanelHeader",
|
||||||
|
"Popover",
|
||||||
|
"PropertyEditor",
|
||||||
|
"PropertyEditorField",
|
||||||
|
"Pulse",
|
||||||
|
"Rating",
|
||||||
|
"Resizable",
|
||||||
|
"SaveIndicator",
|
||||||
|
"SaveIndicatorWrapper",
|
||||||
|
"SchemaEditorCanvas",
|
||||||
|
"SchemaEditorLayout",
|
||||||
|
"SchemaEditorPropertiesPanel",
|
||||||
|
"SchemaEditorSidebar",
|
||||||
|
"SchemaEditorToolbar",
|
||||||
|
"ScrollArea",
|
||||||
|
"SearchBar",
|
||||||
|
"SearchInput",
|
||||||
|
"Sheet",
|
||||||
|
"Sidebar",
|
||||||
|
"Sonner",
|
||||||
|
"Spacer",
|
||||||
|
"Sparkle",
|
||||||
|
"StepIndicator",
|
||||||
|
"Stepper",
|
||||||
|
"StorageSettings",
|
||||||
|
"StorageSettingsWrapper",
|
||||||
|
"Timestamp",
|
||||||
|
"ToolbarActions",
|
||||||
|
"Tooltip",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type JSONUIComponentType = typeof jsonUIComponentTypes[number]
|
||||||
@@ -1,29 +1,6 @@
|
|||||||
export type ComponentType =
|
import type { JSONUIComponentType } from './json-ui-component-types'
|
||||||
| 'div' | 'section' | 'article' | 'header' | 'footer' | 'main'
|
|
||||||
| 'Button' | 'Card' | 'CardHeader' | 'CardTitle' | 'CardDescription' | 'CardContent' | 'CardFooter'
|
export type ComponentType = JSONUIComponentType
|
||||||
| 'Input' | 'TextArea' | 'Textarea' | 'Select' | 'Checkbox' | 'Radio' | 'Switch' | 'Slider' | 'NumberInput' | 'DatePicker' | 'FileUpload'
|
|
||||||
| 'Badge' | 'Progress' | 'Separator' | 'Tabs' | 'TabsContent' | 'TabsList' | 'TabsTrigger' | 'Dialog'
|
|
||||||
| 'Text' | 'Heading' | 'Label' | 'List' | 'ListItem' | 'Grid' | 'Stack' | 'Flex' | 'Container'
|
|
||||||
| 'Link' | 'Breadcrumb' | 'Image' | 'Avatar' | 'Code' | 'Tag' | 'Spinner' | 'Skeleton'
|
|
||||||
| 'CircularProgress' | 'Divider' | 'ProgressBar'
|
|
||||||
| 'Alert' | 'InfoBox' | 'EmptyState' | 'StatusBadge'
|
|
||||||
| 'ErrorBadge' | 'Notification' | 'StatusIcon'
|
|
||||||
| 'Table' | 'TableHeader' | 'TableBody' | 'TableRow' | 'TableCell' | 'TableHead'
|
|
||||||
| 'KeyValue' | 'StatCard' | 'DataCard' | 'SearchInput' | 'ActionBar'
|
|
||||||
| 'DataList' | 'DataTable' | 'MetricCard' | 'Timeline'
|
|
||||||
| 'LazyBarChart' | 'LazyLineChart' | 'LazyD3BarChart' | 'SeedDataManager'
|
|
||||||
| 'SaveIndicator' | 'StorageSettings'
|
|
||||||
| 'AppBranding' | 'LabelWithBadge' | 'NavigationGroupHeader' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState'
|
|
||||||
| 'CodeExplanationDialog' | 'ComponentBindingDialog' | 'DataSourceCard' | 'DataSourceEditorDialog' | 'TreeCard' | 'TreeFormDialog'
|
|
||||||
| 'ToolbarButton'
|
|
||||||
| 'SchemaCodeViewer'
|
|
||||||
| 'FileTabs' | 'NavigationItem' | 'NavigationMenu'
|
|
||||||
| 'EmptyCanvasState' | 'SchemaEditorStatusBar'
|
|
||||||
| 'DataSourceManager' | 'TreeListHeader' | 'TreeListPanel'
|
|
||||||
| 'AppHeader' | 'BindingEditor' | 'CanvasRenderer' | 'ComponentPalette' | 'ComponentTree' | 'EditorActions'
|
|
||||||
| 'EditorToolbar' | 'JSONUIShowcase' | 'LazyInlineMonacoEditor' | 'LazyMonacoEditor' | 'MonacoEditorPanel'
|
|
||||||
| 'PageHeaderContent' | 'PropertyEditor' | 'SchemaEditorCanvas' | 'SchemaEditorLayout'
|
|
||||||
| 'SchemaEditorPropertiesPanel' | 'SchemaEditorSidebar' | 'SchemaEditorToolbar' | 'SearchBar' | 'ToolbarActions'
|
|
||||||
|
|
||||||
export interface BreadcrumbItem {
|
export interface BreadcrumbItem {
|
||||||
label: string
|
label: string
|
||||||
@@ -42,7 +19,7 @@ export type ActionType =
|
|||||||
| 'custom'
|
| 'custom'
|
||||||
|
|
||||||
export type DataSourceType =
|
export type DataSourceType =
|
||||||
| 'kv' | 'computed' | 'static'
|
| 'kv' | 'static'
|
||||||
|
|
||||||
export type BindingSourceType =
|
export type BindingSourceType =
|
||||||
| 'data' | 'bindings' | 'state'
|
| 'data' | 'bindings' | 'state'
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const ComponentSchema: z.ZodType<any> = z.lazy(() =>
|
|||||||
|
|
||||||
export const DataSourceSchema = z.object({
|
export const DataSourceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.enum(['kv', 'computed', 'static', 'ai'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'static', 'ai'], { message: 'Invalid data source type' }),
|
||||||
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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user