mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 22:25:01 +00:00
Compare commits
2 Commits
copilot/re
...
codex/chan
| Author | SHA1 | Date | |
|---|---|---|---|
| 418e3aa657 | |||
| cac24c0716 |
@@ -12,69 +12,7 @@
|
|||||||
"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",
|
||||||
@@ -142,10 +80,7 @@
|
|||||||
"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",
|
||||||
@@ -187,10 +122,7 @@
|
|||||||
"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",
|
||||||
@@ -792,10 +724,7 @@
|
|||||||
"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",
|
||||||
@@ -804,10 +733,7 @@
|
|||||||
"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",
|
||||||
@@ -816,10 +742,7 @@
|
|||||||
"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",
|
||||||
@@ -828,10 +751,7 @@
|
|||||||
"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",
|
||||||
@@ -840,10 +760,7 @@
|
|||||||
"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",
|
||||||
@@ -852,10 +769,7 @@
|
|||||||
"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",
|
||||||
@@ -864,10 +778,7 @@
|
|||||||
"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",
|
||||||
@@ -876,10 +787,7 @@
|
|||||||
"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",
|
||||||
@@ -888,10 +796,7 @@
|
|||||||
"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",
|
||||||
@@ -900,10 +805,7 @@
|
|||||||
"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",
|
||||||
@@ -912,10 +814,7 @@
|
|||||||
"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",
|
||||||
@@ -924,10 +823,7 @@
|
|||||||
"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",
|
||||||
@@ -936,10 +832,7 @@
|
|||||||
"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",
|
||||||
@@ -948,10 +841,7 @@
|
|||||||
"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",
|
||||||
@@ -960,10 +850,7 @@
|
|||||||
"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",
|
||||||
@@ -972,10 +859,7 @@
|
|||||||
"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",
|
||||||
@@ -984,10 +868,7 @@
|
|||||||
"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",
|
||||||
@@ -996,10 +877,7 @@
|
|||||||
"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",
|
||||||
@@ -1008,10 +886,7 @@
|
|||||||
"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",
|
||||||
@@ -1020,10 +895,7 @@
|
|||||||
"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",
|
||||||
@@ -1032,10 +904,7 @@
|
|||||||
"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",
|
||||||
@@ -1044,10 +913,7 @@
|
|||||||
"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",
|
||||||
@@ -1056,10 +922,7 @@
|
|||||||
"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",
|
||||||
@@ -1068,10 +931,7 @@
|
|||||||
"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",
|
||||||
@@ -1080,10 +940,7 @@
|
|||||||
"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",
|
||||||
@@ -1092,10 +949,7 @@
|
|||||||
"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",
|
||||||
@@ -1104,10 +958,7 @@
|
|||||||
"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",
|
||||||
@@ -1116,10 +967,7 @@
|
|||||||
"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",
|
||||||
@@ -1128,10 +976,7 @@
|
|||||||
"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",
|
||||||
@@ -1140,10 +985,7 @@
|
|||||||
"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",
|
||||||
@@ -1152,10 +994,7 @@
|
|||||||
"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",
|
||||||
@@ -1164,10 +1003,7 @@
|
|||||||
"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",
|
||||||
@@ -1176,10 +1012,7 @@
|
|||||||
"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",
|
||||||
@@ -1188,10 +1021,7 @@
|
|||||||
"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",
|
||||||
@@ -1200,10 +1030,7 @@
|
|||||||
"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",
|
||||||
@@ -1212,10 +1039,7 @@
|
|||||||
"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",
|
||||||
@@ -1224,10 +1048,7 @@
|
|||||||
"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",
|
||||||
@@ -1236,10 +1057,7 @@
|
|||||||
"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",
|
||||||
@@ -1457,10 +1275,7 @@
|
|||||||
"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",
|
||||||
@@ -1562,11 +1377,7 @@
|
|||||||
"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",
|
||||||
@@ -1626,10 +1437,7 @@
|
|||||||
"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",
|
||||||
@@ -1652,10 +1460,7 @@
|
|||||||
"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",
|
||||||
@@ -1678,10 +1483,7 @@
|
|||||||
"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",
|
||||||
@@ -1740,10 +1542,7 @@
|
|||||||
"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",
|
||||||
@@ -2030,10 +1829,7 @@
|
|||||||
"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",
|
||||||
@@ -2113,11 +1909,7 @@
|
|||||||
"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",
|
||||||
@@ -2174,11 +1966,7 @@
|
|||||||
"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",
|
||||||
@@ -2261,11 +2049,7 @@
|
|||||||
"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",
|
||||||
@@ -2288,10 +2072,7 @@
|
|||||||
"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",
|
||||||
@@ -2369,11 +2150,7 @@
|
|||||||
"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",
|
||||||
@@ -2459,10 +2236,7 @@
|
|||||||
"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,8 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"kill": "fuser -k 5000/tcp",
|
"kill": "fuser -k 5000/tcp",
|
||||||
"predev": "npm run components:generate-types",
|
"prebuild": "mkdir -p /tmp/dist || true",
|
||||||
"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",
|
||||||
@@ -25,9 +24,8 @@
|
|||||||
"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 && tsx scripts/validate-json-registry.ts"
|
"components:validate": "node scripts/validate-supported-components.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
|
|||||||
@@ -39,13 +39,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "trends",
|
"id": "trends",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"defaultValue": {
|
"compute": "(data) => ({ filesGrowth: 12, modelsGrowth: -3, componentsGrowth: 8, testsGrowth: 15 })",
|
||||||
"filesGrowth": 12,
|
"dependencies": ["metrics"]
|
||||||
"modelsGrowth": -3,
|
|
||||||
"componentsGrowth": 8,
|
|
||||||
"testsGrowth": 15
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -25,12 +25,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filteredFiles",
|
"id": "filteredFiles",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.files",
|
"compute": "(data) => {\n if (!data.searchQuery) return data.files;\n return data.files.filter(f => f.name.toLowerCase().includes(data.searchQuery.toLowerCase()));\n}",
|
||||||
"dependencies": [
|
"dependencies": ["files", "searchQuery"]
|
||||||
"files",
|
|
||||||
"searchQuery"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -22,15 +22,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sourceRoots": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components": {
|
"components": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -82,19 +73,6 @@
|
|||||||
"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": {
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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}`)
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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-component-types.ts')
|
const componentTypesPath = path.join(rootDir, 'src/types/json-ui.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,10 +21,16 @@ 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(componentTypesContent)) !== null) {
|
while ((match = componentTypeRegex.exec(componentTypesBlock)) !== null) {
|
||||||
componentTypeSet.add(match[1])
|
componentTypeSet.add(match[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +1,153 @@
|
|||||||
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { FeatureToggles } from '@/types/project'
|
import { FeatureToggles } from '@/types/project'
|
||||||
import { useMemo } from 'react'
|
import {
|
||||||
import featureToggleSchema from '@/schemas/feature-toggle-settings.json'
|
BookOpen,
|
||||||
import type { PageSchema } from '@/types/json-ui'
|
Code,
|
||||||
import { evaluateExpression } from '@/lib/json-ui/expression-evaluator'
|
Cube,
|
||||||
|
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 =
|
||||||
* FeatureToggleSettings - Now JSON-driven!
|
| 'BookOpen'
|
||||||
*
|
| 'Code'
|
||||||
* This component demonstrates how a complex React component with:
|
| 'Cube'
|
||||||
* - Custom hooks and state management
|
| 'Database'
|
||||||
* - Dynamic data rendering (looping over features)
|
| 'FileText'
|
||||||
* - Event handlers (toggle switches)
|
| 'Flask'
|
||||||
* - Conditional styling (enabled/disabled states)
|
| 'FlowArrow'
|
||||||
*
|
| 'Image'
|
||||||
* Can be converted to a pure JSON schema with custom action handlers.
|
| 'Lightbulb'
|
||||||
* The JSON schema handles all UI structure, data binding, and loops,
|
| 'PaintBrush'
|
||||||
* while custom functions handle business logic.
|
| 'Play'
|
||||||
*
|
| 'Tree'
|
||||||
* Converted from 153 lines of React/TSX to:
|
| 'Wrench'
|
||||||
* - 1 JSON schema file (195 lines, but mostly structure)
|
|
||||||
* - 45 lines of integration code (this file)
|
|
||||||
*
|
|
||||||
* Benefits:
|
|
||||||
* - UI structure is now data-driven and can be modified without code changes
|
|
||||||
* - Feature list is in JSON and can be easily extended
|
|
||||||
* - Styling and layout can be customized via JSON
|
|
||||||
* - Business logic (toggle handler) stays in TypeScript for type safety
|
|
||||||
*/
|
|
||||||
export function FeatureToggleSettings({ features, onFeaturesChange }: FeatureToggleSettingsProps) {
|
|
||||||
// Custom action handler - this is the "hook" that handles complex logic
|
|
||||||
const handlers = useMemo(() => ({
|
|
||||||
updateFeature: (action: any, eventData: any) => {
|
|
||||||
// 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 iconMap: Record<FeatureToggleIconKey, ComponentType<{ size?: number; weight?: 'duotone' }>> = {
|
||||||
const key = evaluateExpression(action.params.key, context) as keyof FeatureToggles
|
BookOpen,
|
||||||
const checked = eventData as boolean
|
Code,
|
||||||
|
Cube,
|
||||||
|
Database,
|
||||||
|
FileText,
|
||||||
|
Flask,
|
||||||
|
FlowArrow,
|
||||||
|
Image,
|
||||||
|
Lightbulb,
|
||||||
|
PaintBrush,
|
||||||
|
Play,
|
||||||
|
Tree,
|
||||||
|
Wrench,
|
||||||
|
}
|
||||||
|
|
||||||
onFeaturesChange({
|
type FeatureToggleItem = {
|
||||||
...features,
|
key: keyof FeatureToggles
|
||||||
[key]: checked,
|
label: string
|
||||||
})
|
description: string
|
||||||
}
|
icon: FeatureToggleIconKey
|
||||||
}), [features, onFeaturesChange])
|
}
|
||||||
|
|
||||||
// Pass features as external data to the JSON renderer
|
const featuresList = featureToggleSettings as FeatureToggleItem[]
|
||||||
const data = useMemo(() => ({ features }), [features])
|
|
||||||
|
|
||||||
|
function FeatureToggleHeader({ enabledCount, totalCount }: { enabledCount: number; totalCount: number }) {
|
||||||
return (
|
return (
|
||||||
<PageRenderer
|
<div className="mb-6">
|
||||||
schema={featureToggleSchema as PageSchema}
|
<h2 className="text-2xl font-bold mb-2">Feature Toggles</h2>
|
||||||
data={data}
|
<p className="text-muted-foreground">
|
||||||
functions={handlers}
|
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) {
|
||||||
|
const handleToggle = (key: keyof FeatureToggles, value: boolean) => {
|
||||||
|
onFeaturesChange({
|
||||||
|
...features,
|
||||||
|
[key]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledCount = Object.values(features).filter(Boolean).length
|
||||||
|
const totalCount = Object.keys(features).length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full p-6 bg-background">
|
||||||
|
<FeatureToggleHeader enabledCount={enabledCount} totalCount={totalCount} />
|
||||||
|
|
||||||
|
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||||
|
<FeatureToggleGrid items={featuresList} features={features} onToggle={handleToggle} />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
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,
|
||||||
@@ -14,22 +27,14 @@ 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.iconId as keyof typeof exampleIcons] || FileCode
|
const icon = exampleIcons[example.icon as keyof typeof exampleIcons] || FileCode
|
||||||
const config = resolveExampleConfig(example.configPath)
|
const config = exampleConfigs[example.configKey as keyof typeof exampleConfigs]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: example.key,
|
key: example.key,
|
||||||
|
|||||||
@@ -45,12 +45,11 @@ 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, ...completionMetrics }}
|
data={props}
|
||||||
|
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, File } from '@phosphor-icons/react'
|
import { Database, Function, File } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceBadgeProps {
|
interface DataSourceBadgeProps {
|
||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
@@ -13,6 +13,11 @@ 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) : data
|
const computedData = computeFn ? computeFn(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, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
import { Card, Badge, 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 } from '@phosphor-icons/react'
|
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceCardProps {
|
interface DataSourceCardProps {
|
||||||
dataSource: DataSource
|
dataSource: DataSource
|
||||||
@@ -11,6 +11,13 @@ 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 (
|
||||||
@@ -20,6 +27,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +59,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} dependent {dependents.length === 1 ? 'source' : 'sources'}
|
Used by {dependents.length} computed {dependents.length === 1 ? 'source' : 'sources'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ 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
|
||||||
}
|
}
|
||||||
@@ -18,13 +20,19 @@ 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,
|
||||||
} = useDataSourceEditor(dataSource)
|
addDependency,
|
||||||
|
removeDependency,
|
||||||
|
availableDeps,
|
||||||
|
selectedDeps,
|
||||||
|
unselectedDeps,
|
||||||
|
} = useDataSourceEditor(dataSource, allDataSources)
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!editingSource) return
|
if (!editingSource) return
|
||||||
@@ -72,6 +80,18 @@ 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>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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, FileText } from '@phosphor-icons/react'
|
import { Database, Function, 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,6 +66,7 @@ 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'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +110,15 @@ 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>
|
||||||
@@ -117,6 +127,7 @@ 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, FileText } from '@phosphor-icons/react'
|
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
import { DataSourceType } from '@/types/json-ui'
|
||||||
|
|
||||||
interface DataSourceManagerHeaderCopy {
|
interface DataSourceManagerHeaderCopy {
|
||||||
@@ -14,6 +14,7 @@ interface DataSourceManagerHeaderCopy {
|
|||||||
addLabel: string
|
addLabel: string
|
||||||
menu: {
|
menu: {
|
||||||
kv: string
|
kv: string
|
||||||
|
computed: string
|
||||||
static: string
|
static: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,10 @@ 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,6 +37,13 @@ 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,
|
||||||
@@ -60,7 +67,7 @@ export function useDataSources(sources: DataSource[]) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (source.type === 'static') {
|
if (source.type === 'static' || source.type === 'computed') {
|
||||||
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', 'static'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'api', 'computed', '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,20 +33,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedTree",
|
"id": "selectedTree",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.trees.find(id === data.selectedTreeId)",
|
"compute": "(data) => data.trees?.find(t => t.id === data.selectedTreeId) || null",
|
||||||
"dependencies": [
|
"dependencies": ["trees", "selectedTreeId"]
|
||||||
"trees",
|
|
||||||
"selectedTreeId"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "treeCount",
|
"id": "treeCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.trees.length",
|
"compute": "(data) => (data.trees || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["trees"]
|
||||||
"trees"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -141,145 +136,55 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "tree-selection-state",
|
"id": "empty-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"conditional": {
|
"props": {
|
||||||
"if": "selectedTree != null",
|
"className": "flex-1 flex items-center justify-center"
|
||||||
"then": {
|
},
|
||||||
"id": "tree-editor",
|
"condition": {
|
||||||
|
"source": "selectedTree",
|
||||||
|
"transform": "(val) => !val"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
"className": "text-center space-y-4"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "tree-header",
|
"id": "empty-state-title",
|
||||||
"type": "div",
|
"type": "Heading",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "mb-6"
|
"className": "text-2xl font-bold text-muted-foreground",
|
||||||
},
|
"children": "No Tree Selected"
|
||||||
"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",
|
"id": "empty-state-description",
|
||||||
"type": "Card",
|
"type": "Text",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "min-h-[500px]"
|
"className": "text-muted-foreground",
|
||||||
},
|
"children": "Select a component tree from the sidebar or create a new one"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"else": {
|
|
||||||
"id": "empty-state",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 flex items-center justify-center"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
{
|
||||||
"id": "empty-state-content",
|
"id": "empty-state-button",
|
||||||
"type": "div",
|
"type": "Button",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"variant": "default",
|
||||||
|
"children": "Create Your First Tree"
|
||||||
},
|
},
|
||||||
"children": [
|
"events": [
|
||||||
{
|
{
|
||||||
"id": "empty-state-title",
|
"event": "click",
|
||||||
"type": "Heading",
|
"actions": [
|
||||||
"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",
|
"id": "open-create-from-empty",
|
||||||
"actions": [
|
"type": "set-value",
|
||||||
{
|
"target": "createDialogOpen",
|
||||||
"id": "open-create-from-empty",
|
"value": true
|
||||||
"type": "set-value",
|
|
||||||
"target": "createDialogOpen",
|
|
||||||
"value": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -287,7 +192,98 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
"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,11 +133,9 @@
|
|||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": "activeFile",
|
"id": "activeFile",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.files.0",
|
"dependencies": ["files", "activeFileId"],
|
||||||
"dependencies": [
|
"compute": "context.files.find(f => f.id === context.activeFileId)"
|
||||||
"files"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
|
|||||||
@@ -35,28 +35,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedBlueprint",
|
"id": "selectedBlueprint",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.flaskConfig.blueprints.find(id === data.selectedBlueprintId)",
|
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.find(b => b.id === data.selectedBlueprintId) || null; }",
|
||||||
"dependencies": [
|
"dependencies": ["flaskConfig", "selectedBlueprintId"]
|
||||||
"flaskConfig",
|
|
||||||
"selectedBlueprintId"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "blueprintCount",
|
"id": "blueprintCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.flaskConfig.blueprints.length",
|
"compute": "(data) => ((data.flaskConfig || {}).blueprints || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["flaskConfig"]
|
||||||
"flaskConfig"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "endpointCount",
|
"id": "endpointCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.selectedBlueprint.endpoints.length",
|
"compute": "(data) => { const bp = data.selectedBlueprint; return bp ? (bp.endpoints || []).length : 0; }",
|
||||||
"dependencies": [
|
"dependencies": ["selectedBlueprint"]
|
||||||
"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,20 +27,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedLambda",
|
"id": "selectedLambda",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.lambdas.find(id === data.selectedLambdaId)",
|
"compute": "(data) => data.lambdas?.find(l => l.id === data.selectedLambdaId) || null",
|
||||||
"dependencies": [
|
"dependencies": ["lambdas", "selectedLambdaId"]
|
||||||
"lambdas",
|
|
||||||
"selectedLambdaId"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lambdaCount",
|
"id": "lambdaCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.lambdas.length",
|
"compute": "(data) => (data.lambdas || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["lambdas"]
|
||||||
"lambdas"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -76,9 +71,7 @@
|
|||||||
"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": [
|
"children": ["Lambdas"]
|
||||||
"Lambdas"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Badge",
|
"type": "Badge",
|
||||||
@@ -140,9 +133,7 @@
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"children": [
|
"children": ["Lambda list will be rendered here"]
|
||||||
"Lambda list will be rendered here"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -168,18 +159,14 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-lg font-semibold mb-2"
|
"className": "text-lg font-semibold mb-2"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": ["No Lambdas Yet"]
|
||||||
"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": [
|
"children": ["Create your first serverless function"]
|
||||||
"Create your first serverless function"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -195,106 +182,101 @@
|
|||||||
},
|
},
|
||||||
"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 != null",
|
"if": "selectedLambda"
|
||||||
"then": {
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex-1 flex items-center justify-center p-8"
|
"className": "max-w-6xl mx-auto w-full space-y-6"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "max-w-6xl mx-auto w-full space-y-6"
|
"className": "flex items-center justify-between"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
|
||||||
"className": "flex items-center justify-between"
|
|
||||||
},
|
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "h1",
|
||||||
"children": [
|
"props": {
|
||||||
{
|
"className": "text-3xl font-bold"
|
||||||
"type": "h1",
|
},
|
||||||
"props": {
|
"bindings": {
|
||||||
"className": "text-3xl font-bold"
|
"children": {
|
||||||
},
|
"source": "selectedLambda",
|
||||||
"bindings": {
|
"path": "name"
|
||||||
"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": "flex-1 flex items-center justify-center p-8"
|
"className": "text-center"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "div",
|
"type": "icon",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center"
|
"name": "Code",
|
||||||
|
"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": [
|
"children": ["No Lambda Selected"]
|
||||||
{
|
},
|
||||||
"type": "icon",
|
{
|
||||||
"props": {
|
"type": "p",
|
||||||
"name": "Code",
|
"props": {
|
||||||
"className": "h-20 w-20 text-muted-foreground/50 mx-auto mb-4",
|
"className": "text-muted-foreground"
|
||||||
"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,20 +28,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedModel",
|
"id": "selectedModel",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.models.find(id === data.selectedModelId)",
|
"compute": "(data) => data.models?.find(m => m.id === data.selectedModelId) || null",
|
||||||
"dependencies": [
|
"dependencies": ["models", "selectedModelId"]
|
||||||
"models",
|
|
||||||
"selectedModelId"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "modelCount",
|
"id": "modelCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.models.length",
|
"compute": "(data) => (data.models || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["models"]
|
||||||
"models"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -136,142 +131,55 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "model-selection-state",
|
"id": "empty-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"conditional": {
|
"props": {
|
||||||
"if": "selectedModel != null",
|
"className": "flex-1 flex items-center justify-center"
|
||||||
"then": {
|
},
|
||||||
"id": "model-editor",
|
"condition": {
|
||||||
|
"source": "selectedModel",
|
||||||
|
"transform": "(val) => !val"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
"className": "text-center space-y-4"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "model-header",
|
"id": "empty-state-title",
|
||||||
"type": "div",
|
"type": "Heading",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "mb-6"
|
"className": "text-2xl font-bold text-muted-foreground",
|
||||||
},
|
"children": "No Model Selected"
|
||||||
"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",
|
"id": "empty-state-description",
|
||||||
"type": "Card",
|
"type": "Text",
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"else": {
|
|
||||||
"id": "empty-state",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
|
||||||
"className": "flex-1 flex items-center justify-center"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "empty-state-content",
|
|
||||||
"type": "div",
|
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"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"
|
||||||
},
|
},
|
||||||
"children": [
|
"events": [
|
||||||
{
|
{
|
||||||
"id": "empty-state-title",
|
"event": "click",
|
||||||
"type": "Heading",
|
"actions": [
|
||||||
"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",
|
"id": "open-create-from-empty",
|
||||||
"actions": [
|
"type": "set-value",
|
||||||
{
|
"target": "createDialogOpen",
|
||||||
"id": "open-create-from-empty",
|
"value": true
|
||||||
"type": "set-value",
|
|
||||||
"target": "createDialogOpen",
|
|
||||||
"value": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -279,7 +187,95 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,15 +5,63 @@
|
|||||||
"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": "SaveIndicator",
|
"type": "div",
|
||||||
"conditional": {
|
"props": {
|
||||||
"if": "lastSaved != null"
|
"className": "flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||||
},
|
},
|
||||||
"bindings": {
|
"conditional": {
|
||||||
"lastSaved": {
|
"if": "lastSaved !== null"
|
||||||
"source": "lastSaved"
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "status-icon",
|
||||||
|
"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,27 +54,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "activeVariant",
|
"id": "activeVariant",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.theme.variants.find(id === data.theme.activeVariantId)",
|
"compute": "(data) => { const theme = data.theme || {}; const variants = theme.variants || []; return variants.find(v => v.id === theme.activeVariantId) || variants[0] || null; }",
|
||||||
"dependencies": [
|
"dependencies": ["theme"]
|
||||||
"theme"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "variantCount",
|
"id": "variantCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.theme.variants.length",
|
"compute": "(data) => ((data.theme || {}).variants || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["theme"]
|
||||||
"theme"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "customColorCount",
|
"id": "customColorCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "Object.keys(data.activeVariant.colors.customColors).length",
|
"compute": "(data) => { const variant = data.activeVariant; if (!variant || !variant.colors) return 0; return Object.keys(variant.colors.customColors || {}).length; }",
|
||||||
"dependencies": [
|
"dependencies": ["activeVariant"]
|
||||||
"activeVariant"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -32,20 +32,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedWorkflow",
|
"id": "selectedWorkflow",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.workflows.find(id === data.selectedWorkflowId)",
|
"compute": "(data) => data.workflows?.find(w => w.id === data.selectedWorkflowId) || null",
|
||||||
"dependencies": [
|
"dependencies": ["workflows", "selectedWorkflowId"]
|
||||||
"workflows",
|
|
||||||
"selectedWorkflowId"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "workflowCount",
|
"id": "workflowCount",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"expression": "data.workflows.length",
|
"compute": "(data) => (data.workflows || []).length",
|
||||||
"dependencies": [
|
"dependencies": ["workflows"]
|
||||||
"workflows"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -76,9 +71,7 @@
|
|||||||
"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": [
|
"children": ["Workflows"]
|
||||||
"Workflows"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "create-button",
|
"id": "create-button",
|
||||||
@@ -124,9 +117,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-sm text-muted-foreground"
|
"className": "text-sm text-muted-foreground"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": ["Status Filter"]
|
||||||
"Status Filter"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -145,9 +136,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"className": "text-center py-8 text-muted-foreground"
|
"className": "text-center py-8 text-muted-foreground"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": ["No workflows yet"]
|
||||||
"No workflows yet"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -161,129 +150,122 @@
|
|||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "workflow-selection-state",
|
"id": "empty-state",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
|
"props": {
|
||||||
|
"className": "flex-1 flex items-center justify-center"
|
||||||
|
},
|
||||||
"conditional": {
|
"conditional": {
|
||||||
"if": "selectedWorkflow != null",
|
"if": "!selectedWorkflow"
|
||||||
"then": {
|
},
|
||||||
"id": "workflow-editor",
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "empty-state-content",
|
||||||
"type": "div",
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex-1 p-6 overflow-auto"
|
"className": "text-center space-y-4"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "workflow-header",
|
"type": "icon",
|
||||||
"type": "div",
|
|
||||||
"props": {
|
"props": {
|
||||||
"className": "mb-6"
|
"name": "GitBranch",
|
||||||
},
|
"className": "h-20 w-20 text-muted-foreground/50 mx-auto",
|
||||||
"children": [
|
"weight": "duotone"
|
||||||
{
|
}
|
||||||
"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",
|
"id": "empty-state-title",
|
||||||
"type": "Card",
|
"type": "h3",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "min-h-[400px] bg-muted/20"
|
"className": "text-2xl font-bold text-muted-foreground"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": ["No Workflow Selected"]
|
||||||
{
|
},
|
||||||
"id": "canvas-content",
|
{
|
||||||
"type": "CardContent",
|
"id": "empty-state-description",
|
||||||
"props": {
|
"type": "p",
|
||||||
"className": "p-6"
|
"props": {
|
||||||
},
|
"className": "text-muted-foreground"
|
||||||
"children": [
|
},
|
||||||
{
|
"children": ["Select a workflow from the sidebar or create a new one"]
|
||||||
"id": "canvas-placeholder",
|
}
|
||||||
"type": "div",
|
]
|
||||||
"props": {
|
}
|
||||||
"className": "text-center text-muted-foreground py-12"
|
]
|
||||||
},
|
},
|
||||||
"children": [
|
{
|
||||||
"Workflow canvas - Add nodes to build your workflow"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"else": {
|
{
|
||||||
"id": "empty-state",
|
"id": "workflow-canvas",
|
||||||
"type": "div",
|
"type": "Card",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "flex-1 flex items-center justify-center"
|
"className": "min-h-[400px] bg-muted/20"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "empty-state-content",
|
"id": "canvas-content",
|
||||||
"type": "div",
|
"type": "CardContent",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "text-center space-y-4"
|
"className": "p-6"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "icon",
|
"id": "canvas-placeholder",
|
||||||
|
"type": "div",
|
||||||
"props": {
|
"props": {
|
||||||
"name": "GitBranch",
|
"className": "text-center text-muted-foreground py-12"
|
||||||
"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": [
|
"children": ["Workflow canvas - Add nodes to build your workflow"]
|
||||||
"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",
|
||||||
"iconId": "ChartBar",
|
"icon": "ChartBar",
|
||||||
"configPath": "/src/config/ui-examples/dashboard.json"
|
"configKey": "dashboard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "form",
|
"key": "form",
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"description": "Dynamic form with validation and data binding",
|
"description": "Dynamic form with validation and data binding",
|
||||||
"iconId": "ListBullets",
|
"icon": "ListBullets",
|
||||||
"configPath": "/src/config/ui-examples/form.json"
|
"configKey": "form"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
"iconId": "Table",
|
"icon": "Table",
|
||||||
"configPath": "/src/config/ui-examples/table.json"
|
"configKey": "table"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
"iconId": "Clock",
|
"icon": "Clock",
|
||||||
"configPath": "/src/config/ui-examples/list-table-timeline.json"
|
"configKey": "list-table-timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "settings",
|
"key": "settings",
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"description": "Tabbed settings panel with switches and selections",
|
"description": "Tabbed settings panel with switches and selections",
|
||||||
"iconId": "Gear",
|
"icon": "Gear",
|
||||||
"configPath": "/src/config/ui-examples/settings.json"
|
"configKey": "settings"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Binding Designer",
|
"title": "Data Binding Designer",
|
||||||
"description": "Connect UI components to KV storage and static data"
|
"description": "Connect UI components to KV storage and computed values"
|
||||||
},
|
},
|
||||||
"bindingsCard": {
|
"bindingsCard": {
|
||||||
"title": "Component Bindings",
|
"title": "Component Bindings",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -33,6 +34,12 @@
|
|||||||
"key": "app-counter",
|
"key": "app-counter",
|
||||||
"defaultValue": 0
|
"defaultValue": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "displayName",
|
||||||
|
"type": "computed",
|
||||||
|
"dependencies": ["userProfile"],
|
||||||
|
"expression": "data.userProfile.name"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -43,8 +50,7 @@
|
|||||||
},
|
},
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"children": {
|
"children": {
|
||||||
"source": "userProfile",
|
"source": "displayName"
|
||||||
"path": "name"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Edit Data Source",
|
"title": "Edit Data Source",
|
||||||
"description": "Configure the data source settings",
|
"description": "Configure the data source settings and dependencies",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": {
|
"id": {
|
||||||
"label": "ID",
|
"label": "ID",
|
||||||
@@ -18,6 +18,17 @@
|
|||||||
"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,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Sources",
|
"title": "Data Sources",
|
||||||
"description": "Manage KV storage and static data"
|
"description": "Manage KV storage, computed values, 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": {
|
||||||
@@ -16,11 +17,12 @@
|
|||||||
},
|
},
|
||||||
"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} dependent {noun}",
|
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
|
||||||
"deleted": "Data source deleted",
|
"deleted": "Data source deleted",
|
||||||
"updated": "Data source updated"
|
"updated": "Data source updated"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useMemo, 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)
|
||||||
|
|
||||||
@@ -14,8 +15,44 @@ 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,6 +9,7 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
|||||||
id: `ds-${Date.now()}`,
|
id: `ds-${Date.now()}`,
|
||||||
type,
|
type,
|
||||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||||
|
...(type === 'computed' && { expression: '', dependencies: [] }),
|
||||||
...(type === 'static' && { defaultValue: null }),
|
...(type === 'static' && { defaultValue: null }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ 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,11 +1,13 @@
|
|||||||
import { useKV } from '@/hooks/use-kv'
|
import { useKV } from '@/hooks/use-kv'
|
||||||
|
|
||||||
export type DataSourceType = 'kv' | 'static'
|
export type DataSourceType = 'kv' | 'static' | 'computed'
|
||||||
|
|
||||||
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) {
|
||||||
@@ -16,6 +18,13 @@ 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 derivedSources = dataSources.filter(ds => ds.expression || ds.valueTemplate)
|
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
||||||
|
|
||||||
derivedSources.forEach(source => {
|
computedSources.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 derivedValue = source.expression
|
const computedValue = 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]: derivedValue }))
|
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [data, dataSources])
|
}, [data, dataSources])
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
[dataSources]
|
[dataSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
const derivedSources = useMemo(
|
const computedSources = useMemo(
|
||||||
() => dataSources.filter((ds) => ds.expression || ds.valueTemplate),
|
() => dataSources.filter((ds) => ds.type === 'computed'),
|
||||||
[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> = {}
|
||||||
|
|
||||||
derivedSources.forEach((ds) => {
|
computedSources.forEach((ds) => {
|
||||||
const evaluationContext = { data: { ...data, ...result } }
|
const evaluationContext = { data }
|
||||||
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
|
||||||
}, [derivedSources, data])
|
}, [computedSources, data])
|
||||||
|
|
||||||
const allData = useMemo(
|
const allData = useMemo(
|
||||||
() => ({ ...data, ...computedData }),
|
() => ({ ...data, ...computedData }),
|
||||||
|
|||||||
@@ -47,20 +47,16 @@ export function usePage(schema: PageSchema) {
|
|||||||
const computed: Record<string, any> = {}
|
const computed: Record<string, any> = {}
|
||||||
|
|
||||||
schema.data.forEach(source => {
|
schema.data.forEach(source => {
|
||||||
if (source.expression) {
|
if (source.type === 'computed') {
|
||||||
computed[source.id] = evaluateBindingExpression(source.expression, { ...dataContext, ...computed }, {
|
if (source.expression) {
|
||||||
fallback: undefined,
|
computed[source.id] = evaluateBindingExpression(source.expression, dataContext, {
|
||||||
label: `derived data (${source.id})`,
|
fallback: undefined,
|
||||||
})
|
label: `computed data (${source.id})`,
|
||||||
return
|
})
|
||||||
}
|
} else if (source.valueTemplate) {
|
||||||
|
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: dataContext })
|
||||||
if (source.valueTemplate) {
|
}
|
||||||
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: { ...dataContext, ...computed } })
|
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === 'static' && source.defaultValue !== undefined) {
|
|
||||||
computed[source.id] = source.defaultValue
|
computed[source.id] = source.defaultValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useMemo, 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) {
|
||||||
@@ -25,15 +27,51 @@ 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,101 +2,6 @@
|
|||||||
|
|
||||||
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> {
|
||||||
@@ -124,13 +29,8 @@ Make sure to use appropriate Material UI components and props. Keep the structur
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = parseAndValidateJson(
|
const parsed = JSON.parse(result)
|
||||||
result,
|
return parsed.component
|
||||||
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) {
|
||||||
@@ -180,13 +80,8 @@ Return a valid JSON object with a single property "model" containing the model s
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = parseAndValidateJson(
|
const parsed = JSON.parse(result)
|
||||||
result,
|
return parsed.model
|
||||||
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) {
|
||||||
@@ -277,13 +172,8 @@ Return a valid JSON object with a single property "theme" containing:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = parseAndValidateJson(
|
const parsed = JSON.parse(result)
|
||||||
result,
|
return parsed.theme
|
||||||
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) {
|
||||||
@@ -312,13 +202,8 @@ Suggest 3-5 common fields that would be useful for this model type. Use camelCas
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const parsed = parseAndValidateJson(
|
const parsed = JSON.parse(result)
|
||||||
result,
|
return parsed.fields
|
||||||
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) {
|
||||||
@@ -399,12 +284,7 @@ Create 2-4 essential files for the app structure. Include appropriate Prisma mod
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return parseAndValidateJson(
|
return JSON.parse(result)
|
||||||
result,
|
|
||||||
completeAppResponseSchema,
|
|
||||||
'generate-app',
|
|
||||||
'AI app generation response was invalid. Please retry with more detail.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
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,6 +1,63 @@
|
|||||||
import { ComponentType } from 'react'
|
import { ComponentType } from 'react'
|
||||||
import * as PhosphorIcons from '@phosphor-icons/react'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { InputOtp } from '@/components/ui/input-otp'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Alert as ShadcnAlert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { AlertDialog } from '@/components/ui/alert-dialog'
|
||||||
|
import { AspectRatio } from '@/components/ui/aspect-ratio'
|
||||||
|
import { Carousel } from '@/components/ui/carousel'
|
||||||
|
import { ChartContainer as Chart } from '@/components/ui/chart'
|
||||||
|
import { Collapsible } from '@/components/ui/collapsible'
|
||||||
|
import { Command } from '@/components/ui/command'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { DropdownMenu } from '@/components/ui/dropdown-menu'
|
||||||
|
import { Menubar } from '@/components/ui/menubar'
|
||||||
|
import { NavigationMenu } from '@/components/ui/navigation-menu'
|
||||||
|
import { Table as ShadcnTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
import { ResizablePanelGroup as Resizable } from '@/components/ui/resizable'
|
||||||
|
import { Sheet } from '@/components/ui/sheet'
|
||||||
|
import { Sidebar } from '@/components/ui/sidebar'
|
||||||
|
import { Toaster as Sonner } from '@/components/ui/sonner'
|
||||||
|
import { ToggleGroup } from '@/components/ui/toggle-group'
|
||||||
|
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
|
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
||||||
|
import * as AtomComponents from '@/components/atoms'
|
||||||
|
import * as MoleculeComponents from '@/components/molecules'
|
||||||
|
import * as OrganismComponents from '@/components/organisms'
|
||||||
|
import {
|
||||||
|
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>
|
||||||
@@ -15,16 +72,11 @@ 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 {
|
||||||
@@ -33,127 +85,70 @@ export interface DeprecatedComponentInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
||||||
const sourceRoots = jsonRegistry.sourceRoots ?? {}
|
|
||||||
const moduleMapsBySource = Object.fromEntries(
|
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
|
||||||
Object.entries(sourceRoots).map(([source, patterns]) => {
|
entry.export ?? entry.name ?? entry.type
|
||||||
if (!patterns || patterns.length === 0) {
|
|
||||||
return [source, {}]
|
const buildRegistryFromNames = (
|
||||||
|
names: string[],
|
||||||
|
components: Record<string, ComponentType<any>>
|
||||||
|
): UIComponentRegistry => {
|
||||||
|
return names.reduce<UIComponentRegistry>((registry, name) => {
|
||||||
|
const component = components[name]
|
||||||
|
if (component) {
|
||||||
|
registry[name] = component
|
||||||
}
|
}
|
||||||
return [source, import.meta.glob(patterns, { eager: true })]
|
return registry
|
||||||
})
|
}, {})
|
||||||
) 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 entryKey = getRegistryEntryKey(entry)
|
const entryName = getRegistryEntryName(entry)
|
||||||
return entryKey ? [entryKey, entry] : null
|
return entryName ? [entryName, 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 entryKey = getRegistryEntryKey(entry)
|
const entryName = getRegistryEntryName(entry)
|
||||||
if (!entryKey) {
|
if (!entryName) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
if (entry.status === 'deprecated' || entry.deprecated) {
|
if (entry.status === 'deprecated' || entry.deprecated) {
|
||||||
acc[entryKey] = entry.deprecated ?? {}
|
acc[entryName] = entry.deprecated ?? {}
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
const atomRegistryNames = jsonRegistryEntries
|
||||||
const buildComponentMapFromExports = (
|
.filter((entry) => entry.source === 'atoms')
|
||||||
exports: Record<string, unknown>
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
): Record<string, ComponentType<any>> => {
|
.filter((name): name is string => Boolean(name))
|
||||||
return Object.entries(exports).reduce<Record<string, ComponentType<any>>>((acc, [key, value]) => {
|
const moleculeRegistryNames = jsonRegistryEntries
|
||||||
if (value && (typeof value === 'function' || typeof value === 'object')) {
|
.filter((entry) => entry.source === 'molecules')
|
||||||
acc[key] = value as ComponentType<any>
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
}
|
.filter((name): name is string => Boolean(name))
|
||||||
return acc
|
const organismRegistryNames = jsonRegistryEntries
|
||||||
}, {})
|
.filter((entry) => entry.source === 'organisms')
|
||||||
}
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
|
.filter((name): name is string => Boolean(name))
|
||||||
const buildComponentMapFromModules = (
|
const shadcnRegistryNames = jsonRegistryEntries
|
||||||
modules: Record<string, unknown>
|
.filter((entry) => entry.source === 'ui')
|
||||||
): Record<string, ComponentType<any>> => {
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
return Object.values(modules).reduce<Record<string, ComponentType<any>>>((acc, moduleExports) => {
|
.filter((name): name is string => Boolean(name))
|
||||||
if (!moduleExports || typeof moduleExports !== 'object') {
|
const wrapperRegistryNames = jsonRegistryEntries
|
||||||
return acc
|
.filter((entry) => entry.source === 'wrappers')
|
||||||
}
|
.map((entry) => getRegistryEntryName(entry))
|
||||||
Object.entries(buildComponentMapFromExports(moduleExports as Record<string, unknown>)).forEach(
|
.filter((name): name is string => Boolean(name))
|
||||||
([key, component]) => {
|
const iconRegistryNames = jsonRegistryEntries
|
||||||
acc[key] = component
|
.filter((entry) => entry.source === 'icons')
|
||||||
}
|
.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,
|
||||||
@@ -174,33 +169,173 @@ export const primitiveComponents: UIComponentRegistry = {
|
|||||||
nav: 'nav' as any,
|
nav: 'nav' as any,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shadcnComponents: UIComponentRegistry = buildRegistryFromEntries(
|
const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
||||||
'ui',
|
AlertDialog,
|
||||||
uiComponentMap
|
AspectRatio,
|
||||||
|
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 = buildRegistryFromEntries(
|
export const atomComponents: UIComponentRegistry = {
|
||||||
'atoms',
|
...buildRegistryFromNames(
|
||||||
atomComponentMap
|
atomRegistryNames,
|
||||||
|
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>>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const moleculeComponents: UIComponentRegistry = buildRegistryFromEntries(
|
const wrapperComponentMap: Record<string, ComponentType<any>> = {
|
||||||
'molecules',
|
ComponentBindingDialogWrapper,
|
||||||
moleculeComponentMap
|
ComponentTreeWrapper,
|
||||||
)
|
DataSourceEditorDialogWrapper,
|
||||||
|
GitHubBuildStatusWrapper,
|
||||||
|
SaveIndicatorWrapper,
|
||||||
|
LazyBarChartWrapper,
|
||||||
|
LazyLineChartWrapper,
|
||||||
|
LazyD3BarChartWrapper,
|
||||||
|
SeedDataManagerWrapper,
|
||||||
|
StorageSettingsWrapper,
|
||||||
|
}
|
||||||
|
|
||||||
export const organismComponents: UIComponentRegistry = buildRegistryFromEntries(
|
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||||
'organisms',
|
wrapperRegistryNames,
|
||||||
organismComponentMap
|
|
||||||
)
|
|
||||||
|
|
||||||
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromEntries(
|
|
||||||
'wrappers',
|
|
||||||
wrapperComponentMap
|
wrapperComponentMap
|
||||||
)
|
)
|
||||||
|
|
||||||
export const iconComponents: UIComponentRegistry = buildRegistryFromEntries(
|
const iconComponentMap: Record<string, ComponentType<any>> = {
|
||||||
'icons',
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Search: MagnifyingGlass,
|
||||||
|
Filter: Funnel,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
Edit: PencilSimple,
|
||||||
|
Trash,
|
||||||
|
Eye,
|
||||||
|
EyeOff: EyeClosed,
|
||||||
|
ChevronUp: CaretUp,
|
||||||
|
ChevronDown: CaretDown,
|
||||||
|
ChevronLeft: CaretLeft,
|
||||||
|
ChevronRight: CaretRight,
|
||||||
|
Settings: Gear,
|
||||||
|
User,
|
||||||
|
Bell,
|
||||||
|
Mail: Envelope,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
Star,
|
||||||
|
Heart,
|
||||||
|
Share: ShareNetwork,
|
||||||
|
Link: LinkSimple,
|
||||||
|
Copy,
|
||||||
|
Save: FloppyDisk,
|
||||||
|
RefreshCw: ArrowClockwise,
|
||||||
|
AlertCircle: WarningCircle,
|
||||||
|
Info,
|
||||||
|
HelpCircle: Question,
|
||||||
|
Home: House,
|
||||||
|
Menu: ListIcon,
|
||||||
|
MoreVertical: DotsThreeVertical,
|
||||||
|
MoreHorizontal: DotsThree,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const iconComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||||
|
iconRegistryNames,
|
||||||
iconComponentMap
|
iconComponentMap
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,35 +54,6 @@ 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,6 +8,7 @@ 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
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ 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', 'static']),
|
type: z.enum(['kv', 'api', 'computed', 'static']),
|
||||||
config: z.any(),
|
config: z.any(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
})
|
})
|
||||||
@@ -241,6 +241,13 @@ 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,31 +1,14 @@
|
|||||||
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,7 +4,6 @@ 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
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
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,7 +2,6 @@ interface RateLimitConfig {
|
|||||||
maxRequests: number
|
maxRequests: number
|
||||||
windowMs: number
|
windowMs: number
|
||||||
retryDelay: number
|
retryDelay: number
|
||||||
maxRetries?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestRecord {
|
interface RequestRecord {
|
||||||
@@ -10,15 +9,14 @@ interface RequestRecord {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RateLimiter {
|
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
|
||||||
}
|
}
|
||||||
@@ -28,60 +26,49 @@ export 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 maxRetries = this.config.maxRetries ?? 3
|
const now = Date.now()
|
||||||
let attempts = 0
|
const record = this.requests.get(key)
|
||||||
|
|
||||||
while (true) {
|
if (record) {
|
||||||
const now = Date.now()
|
const timeElapsed = now - record.timestamp
|
||||||
const record = this.requests.get(key)
|
|
||||||
let isLimited = false
|
|
||||||
|
|
||||||
if (record) {
|
if (timeElapsed < this.config.windowMs) {
|
||||||
const timeElapsed = now - record.timestamp
|
if (record.count >= this.config.maxRequests) {
|
||||||
|
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 {
|
|
||||||
this.requests.set(key, { timestamp: now, count: 1 })
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
if (isLimited) {
|
try {
|
||||||
if (priority === 'high' && attempts < maxRetries) {
|
return await fn()
|
||||||
attempts += 1
|
} catch (error) {
|
||||||
await new Promise(resolve => setTimeout(resolve, this.config.retryDelay))
|
if (error instanceof Error && (
|
||||||
continue
|
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)
|
||||||
|
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,18 +24,22 @@ 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) {
|
||||||
const errorPayload = hasJsonBody ? await response.json().catch(() => null) : null
|
let errorMessage = response.statusText
|
||||||
const errorMessage = errorPayload?.error || response.statusText || `HTTP ${response.status}`
|
try {
|
||||||
throw new Error(errorMessage)
|
const errorText = await response.text()
|
||||||
}
|
if (errorText) {
|
||||||
|
try {
|
||||||
if (response.status === 204 || !hasJsonBody) {
|
const parsed = JSON.parse(errorText) as { error?: string }
|
||||||
return undefined as T
|
errorMessage = parsed.error || errorText
|
||||||
|
} 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": "static",
|
"type": "computed",
|
||||||
"expression": "data.users",
|
"expression": "data.users",
|
||||||
"dependencies": ["users", "filterQuery"]
|
"dependencies": ["users", "filterQuery"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "static",
|
"type": "computed",
|
||||||
"valueTemplate": {
|
"valueTemplate": {
|
||||||
"total": "data.users.length",
|
"total": "data.users.length",
|
||||||
"active": "data.users.filter(status === 'active').length",
|
"active": "data.users.filter(status === 'active').length",
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "static",
|
"type": "computed",
|
||||||
"valueTemplate": {
|
"valueTemplate": {
|
||||||
"total": "data.todos.length",
|
"total": "data.todos.length",
|
||||||
"completed": "data.todos.filter(completed === true).length",
|
"completed": "data.todos.filter(completed === true).length",
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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',
|
|
||||||
])
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
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
|
||||||
@@ -10,7 +9,7 @@ interface AutoSyncConfig {
|
|||||||
maxQueueSize: number
|
maxQueueSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AutoSyncManager {
|
class AutoSyncManager {
|
||||||
private config: AutoSyncConfig = {
|
private config: AutoSyncConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
intervalMs: 30000,
|
intervalMs: 30000,
|
||||||
@@ -21,8 +20,6 @@ export 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>) {
|
||||||
@@ -71,33 +68,18 @@ export 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()
|
||||||
@@ -145,7 +127,28 @@ export const createAutoSyncMiddleware = (): Middleware => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemChangeActionTypes.has(action.type)) {
|
const changeActions = [
|
||||||
|
'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,7 +37,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
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,11 +2,6 @@ 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
|
||||||
@@ -43,23 +38,10 @@ 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}`
|
||||||
@@ -80,12 +62,7 @@ class PersistenceQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processQueue() {
|
async processQueue() {
|
||||||
if (this.processing) {
|
if (this.processing || this.queue.size === 0) return
|
||||||
this.pendingFlush = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.queue.size === 0) return
|
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
@@ -98,10 +75,14 @@ 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)
|
||||||
await this.syncToFlaskWithRetry(op, op.value)
|
if (sliceToPersistenceMap[op.storeName]?.syncToFlask) {
|
||||||
|
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)
|
||||||
await this.syncToFlaskWithRetry(op, null)
|
if (sliceToPersistenceMap[op.storeName]?.syncToFlask) {
|
||||||
|
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)
|
||||||
@@ -116,23 +97,6 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,89 +107,6 @@ 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()
|
||||||
@@ -247,7 +128,10 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
if (!sliceState) return result
|
if (!sliceState) return result
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (persistenceSingleItemActionNames.has(actionName)) {
|
if (actionName === 'addItem' || actionName === 'updateItem' || actionName === 'saveFile' ||
|
||||||
|
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({
|
||||||
@@ -260,7 +144,10 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persistenceBulkActionNames.has(actionName)) {
|
if (actionName === 'addItems' || actionName === 'setItems' || actionName === 'setFiles' ||
|
||||||
|
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) => {
|
||||||
@@ -277,7 +164,10 @@ export const createPersistenceMiddleware = (): Middleware => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persistenceDeleteActionNames.has(actionName)) {
|
if (actionName === 'removeItem' || actionName === 'deleteFile' || actionName === 'deleteModel' ||
|
||||||
|
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({
|
||||||
@@ -318,8 +208,6 @@ 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,18 +107,21 @@ 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 && action.meta?.requestId) {
|
if (isPendingAction) {
|
||||||
syncMonitor.startOperation(action.meta.requestId)
|
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
||||||
|
syncMonitor.startOperation(operationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = next(action)
|
const result = next(action)
|
||||||
|
|
||||||
if (isFulfilledAction && action.meta?.requestId) {
|
if (isFulfilledAction) {
|
||||||
syncMonitor.endOperation(action.meta.requestId, true)
|
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
||||||
|
syncMonitor.endOperation(operationId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRejectedAction && action.meta?.requestId) {
|
if (isRejectedAction) {
|
||||||
syncMonitor.endOperation(action.meta.requestId, false)
|
const operationId = action.meta?.requestId || `${action.type}-${Date.now()}`
|
||||||
|
syncMonitor.endOperation(operationId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
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,8 +9,6 @@ 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
|
||||||
@@ -70,51 +68,15 @@ 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 (SYNCABLE_STORES.has(storeName)) {
|
if (storeName === 'files' ||
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
// 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,6 +1,29 @@
|
|||||||
import type { JSONUIComponentType } from './json-ui-component-types'
|
export type ComponentType =
|
||||||
|
| 'div' | 'section' | 'article' | 'header' | 'footer' | 'main'
|
||||||
export type ComponentType = JSONUIComponentType
|
| 'Button' | 'Card' | 'CardHeader' | 'CardTitle' | 'CardDescription' | 'CardContent' | 'CardFooter'
|
||||||
|
| '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
|
||||||
@@ -19,7 +42,7 @@ export type ActionType =
|
|||||||
| 'custom'
|
| 'custom'
|
||||||
|
|
||||||
export type DataSourceType =
|
export type DataSourceType =
|
||||||
| 'kv' | 'static'
|
| 'kv' | 'computed' | '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', 'static', 'ai'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'computed', '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