mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
Merge pull request #197 from johndoe6345789/codex/refactor-computed-json-configs
Remove legacy `computed` data sources; migrate to expression/valueTemplate and update UI
This commit is contained in:
@@ -39,9 +39,13 @@
|
||||
},
|
||||
{
|
||||
"id": "trends",
|
||||
"type": "computed",
|
||||
"compute": "(data) => ({ filesGrowth: 12, modelsGrowth: -3, componentsGrowth: 8, testsGrowth: 15 })",
|
||||
"dependencies": ["metrics"]
|
||||
"type": "static",
|
||||
"defaultValue": {
|
||||
"filesGrowth": 12,
|
||||
"modelsGrowth": -3,
|
||||
"componentsGrowth": 8,
|
||||
"testsGrowth": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
},
|
||||
{
|
||||
"id": "filteredFiles",
|
||||
"type": "computed",
|
||||
"compute": "(data) => {\n if (!data.searchQuery) return data.files;\n return data.files.filter(f => f.name.toLowerCase().includes(data.searchQuery.toLowerCase()));\n}",
|
||||
"dependencies": ["files", "searchQuery"]
|
||||
"type": "static",
|
||||
"expression": "data.files",
|
||||
"dependencies": [
|
||||
"files",
|
||||
"searchQuery"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -45,11 +45,12 @@ function getCompletionMessage(score: number): string {
|
||||
}
|
||||
|
||||
export function ProjectDashboard(props: ProjectDashboardProps) {
|
||||
const completionMetrics = calculateCompletionScore(props)
|
||||
|
||||
return (
|
||||
<JSONPageRenderer
|
||||
schema={dashboardSchema as any}
|
||||
data={props}
|
||||
functions={{ calculateCompletionScore }}
|
||||
data={{ ...props, ...completionMetrics }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
import { Database, Function, File } from '@phosphor-icons/react'
|
||||
import { Database, File } from '@phosphor-icons/react'
|
||||
|
||||
interface DataSourceBadgeProps {
|
||||
type: DataSourceType
|
||||
@@ -13,11 +13,6 @@ const dataSourceConfig = {
|
||||
label: 'KV Storage',
|
||||
className: 'bg-accent/20 text-accent border-accent/30'
|
||||
},
|
||||
computed: {
|
||||
icon: Function,
|
||||
label: 'Computed',
|
||||
className: 'bg-primary/20 text-primary border-primary/30'
|
||||
},
|
||||
static: {
|
||||
icon: File,
|
||||
label: 'Static',
|
||||
|
||||
@@ -108,7 +108,7 @@ function PageCard({ card, data, functions }: PageCardProps) {
|
||||
|
||||
if (card.type === 'gradient-card') {
|
||||
const computeFn = functions[card.dataSource?.compute]
|
||||
const computedData = computeFn ? computeFn(data) : {}
|
||||
const computedData = computeFn ? computeFn(data) : data
|
||||
|
||||
return (
|
||||
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card, Badge, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
||||
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||
|
||||
interface DataSourceCardProps {
|
||||
dataSource: DataSource
|
||||
@@ -11,13 +11,6 @@ interface DataSourceCardProps {
|
||||
}
|
||||
|
||||
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
||||
const getDependencyCount = () => {
|
||||
if (dataSource.type === 'computed') {
|
||||
return dataSource.dependencies?.length || 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const renderTypeSpecificInfo = () => {
|
||||
if (dataSource.type === 'kv') {
|
||||
return (
|
||||
@@ -27,18 +20,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
)
|
||||
}
|
||||
|
||||
if (dataSource.type === 'computed') {
|
||||
const depCount = getDependencyCount()
|
||||
return (
|
||||
<Flex align="center" gap="sm">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<ArrowsDownUp className="w-3 h-3 mr-1" />
|
||||
{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -59,7 +40,7 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
{dependents.length > 0 && (
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<Text variant="caption">
|
||||
Used by {dependents.length} computed {dependents.length === 1 ? 'source' : 'sources'}
|
||||
Used by {dependents.length} dependent {dependents.length === 1 ? 'source' : 'sources'}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,14 +5,12 @@ import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||
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 { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||
|
||||
interface DataSourceEditorDialogProps {
|
||||
open: boolean
|
||||
dataSource: DataSource | null
|
||||
allDataSources: DataSource[]
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (dataSource: DataSource) => void
|
||||
}
|
||||
@@ -20,19 +18,13 @@ interface DataSourceEditorDialogProps {
|
||||
export function DataSourceEditorDialog({
|
||||
open,
|
||||
dataSource,
|
||||
allDataSources,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: DataSourceEditorDialogProps) {
|
||||
const {
|
||||
editingSource,
|
||||
updateField,
|
||||
addDependency,
|
||||
removeDependency,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
} = useDataSourceEditor(dataSource, allDataSources)
|
||||
} = useDataSourceEditor(dataSource)
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editingSource) return
|
||||
@@ -80,18 +72,6 @@ export function DataSourceEditorDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingSource.type === 'computed' && (
|
||||
<ComputedSourceFields
|
||||
editingSource={editingSource}
|
||||
availableDeps={availableDeps}
|
||||
selectedDeps={selectedDeps}
|
||||
unselectedDeps={unselectedDeps}
|
||||
copy={dataSourceEditorCopy.computed}
|
||||
onUpdateField={updateField}
|
||||
onAddDependency={addDependency}
|
||||
onRemoveDependency={removeDependency}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
|
||||
interface ComputedSourceFieldsCopy {
|
||||
expressionLabel: string
|
||||
expressionPlaceholder: string
|
||||
expressionHelp: string
|
||||
valueTemplateLabel: string
|
||||
valueTemplatePlaceholder: string
|
||||
valueTemplateHelp: string
|
||||
dependenciesLabel: string
|
||||
availableSourcesLabel: string
|
||||
emptyDependencies: string
|
||||
}
|
||||
|
||||
interface ComputedSourceFieldsProps {
|
||||
editingSource: DataSource
|
||||
availableDeps: DataSource[]
|
||||
selectedDeps: string[]
|
||||
unselectedDeps: DataSource[]
|
||||
copy: ComputedSourceFieldsCopy
|
||||
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
|
||||
onAddDependency: (depId: string) => void
|
||||
onRemoveDependency: (depId: string) => void
|
||||
}
|
||||
|
||||
export function ComputedSourceFields({
|
||||
editingSource,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
copy,
|
||||
onUpdateField,
|
||||
onAddDependency,
|
||||
onRemoveDependency,
|
||||
}: ComputedSourceFieldsProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.expressionLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.expression || ''}
|
||||
onChange={(e) => {
|
||||
onUpdateField('expression', e.target.value)
|
||||
}}
|
||||
placeholder={copy.expressionPlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.expressionHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.valueTemplateLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.valueTemplate ? JSON.stringify(editingSource.valueTemplate, null, 2) : ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const template = JSON.parse(e.target.value)
|
||||
onUpdateField('valueTemplate', template)
|
||||
} catch (err) {
|
||||
// Invalid JSON
|
||||
}
|
||||
}}
|
||||
placeholder={copy.valueTemplatePlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.valueTemplateHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.dependenciesLabel}</Label>
|
||||
|
||||
{selectedDeps.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
|
||||
{selectedDeps.map(depId => (
|
||||
<Badge
|
||||
key={depId}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{depId}
|
||||
<button
|
||||
onClick={() => onRemoveDependency(depId)}
|
||||
className="ml-1 hover:text-destructive"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unselectedDeps.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{copy.availableSourcesLabel}</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{unselectedDeps.map(ds => (
|
||||
<Button
|
||||
key={ds.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onAddDependency(ds.id)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
+ {ds.id}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableDeps.length === 0 && selectedDeps.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{copy.emptyDependencies}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
||||
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
import { Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { Database, FileText } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { EmptyState, Stack } from '@/components/atoms'
|
||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||
@@ -66,7 +66,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
|
||||
const groupedSources = {
|
||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||
computed: localSources.filter(ds => ds.type === 'computed'),
|
||||
static: localSources.filter(ds => ds.type === 'static'),
|
||||
}
|
||||
|
||||
@@ -110,15 +109,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
|
||||
<DataSourceGroupSection
|
||||
icon={<Function size={16} />}
|
||||
label={dataSourceManagerCopy.groups.computed}
|
||||
dataSources={groupedSources.computed}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -127,7 +117,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
<DataSourceEditorDialog
|
||||
open={dialogOpen}
|
||||
dataSource={editingSource}
|
||||
allDataSources={localSources}
|
||||
onOpenChange={setDialogOpen}
|
||||
onSave={handleSaveSource}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { Plus, Database, FileText } from '@phosphor-icons/react'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
|
||||
interface DataSourceManagerHeaderCopy {
|
||||
@@ -14,7 +14,6 @@ interface DataSourceManagerHeaderCopy {
|
||||
addLabel: string
|
||||
menu: {
|
||||
kv: string
|
||||
computed: string
|
||||
static: string
|
||||
}
|
||||
}
|
||||
@@ -49,10 +48,6 @@ export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeader
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{copy.menu.kv}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAdd('computed')}>
|
||||
<Function className="w-4 h-4 mr-2" />
|
||||
{copy.menu.computed}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAdd('static')}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{copy.menu.static}
|
||||
|
||||
@@ -37,13 +37,6 @@ export function useDataSource(source: DataSource) {
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
case 'computed':
|
||||
return {
|
||||
data: source.defaultValue,
|
||||
setData: () => {},
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
data: null,
|
||||
@@ -67,7 +60,7 @@ export function useDataSources(sources: DataSource[]) {
|
||||
|
||||
useEffect(() => {
|
||||
sources.forEach((source) => {
|
||||
if (source.type === 'static' || source.type === 'computed') {
|
||||
if (source.type === 'static') {
|
||||
updateData(source.id, source.defaultValue)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ActionSchema = z.object({
|
||||
|
||||
export const DataSourceSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(['kv', 'api', 'computed', 'static'], { message: 'Invalid data source type' }),
|
||||
type: z.enum(['kv', 'api', 'static'], { message: 'Invalid data source type' }),
|
||||
key: z.string().optional(),
|
||||
endpoint: z.string().optional(),
|
||||
transform: z.string().optional(),
|
||||
|
||||
@@ -33,15 +33,20 @@
|
||||
},
|
||||
{
|
||||
"id": "selectedTree",
|
||||
"type": "computed",
|
||||
"compute": "(data) => data.trees?.find(t => t.id === data.selectedTreeId) || null",
|
||||
"dependencies": ["trees", "selectedTreeId"]
|
||||
"type": "static",
|
||||
"expression": "data.trees.find(id === data.selectedTreeId)",
|
||||
"dependencies": [
|
||||
"trees",
|
||||
"selectedTreeId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "treeCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => (data.trees || []).length",
|
||||
"dependencies": ["trees"]
|
||||
"type": "static",
|
||||
"expression": "data.trees.length",
|
||||
"dependencies": [
|
||||
"trees"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@@ -136,55 +141,145 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state",
|
||||
"id": "tree-selection-state",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center"
|
||||
},
|
||||
"condition": {
|
||||
"source": "selectedTree",
|
||||
"transform": "(val) => !val"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-content",
|
||||
"conditional": {
|
||||
"if": "selectedTree != null",
|
||||
"then": {
|
||||
"id": "tree-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center space-y-4"
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "Heading",
|
||||
"id": "tree-header",
|
||||
"type": "div",
|
||||
"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"
|
||||
"className": "mb-6"
|
||||
},
|
||||
"events": [
|
||||
"children": [
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
"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": "open-create-from-empty",
|
||||
"type": "set-value",
|
||||
"target": "createDialogOpen",
|
||||
"value": true
|
||||
"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",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-2xl font-bold text-muted-foreground",
|
||||
"children": "No Tree Selected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-description",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground",
|
||||
"children": "Select a component tree from the sidebar or create a new one"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-button",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "default",
|
||||
"children": "Create Your First Tree"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
{
|
||||
"id": "open-create-from-empty",
|
||||
"type": "set-value",
|
||||
"target": "createDialogOpen",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -192,98 +287,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tree-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"condition": {
|
||||
"source": "selectedTree",
|
||||
"transform": "(val) => !!val"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tree-header",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tree-name",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold mb-2"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedTree",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tree-description",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedTree",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tree-canvas",
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"className": "min-h-[500px]"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-title",
|
||||
"type": "CardTitle",
|
||||
"props": {
|
||||
"children": "Component Hierarchy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "canvas-description",
|
||||
"type": "CardDescription",
|
||||
"props": {
|
||||
"children": "Build your component tree structure"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "canvas-content",
|
||||
"type": "CardContent",
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-placeholder",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center text-muted-foreground py-12 border-2 border-dashed border-border rounded-lg",
|
||||
"children": "Component tree builder - Add components to build your hierarchy"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -291,4 +295,4 @@
|
||||
}
|
||||
],
|
||||
"globalActions": []
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
"title": "Project Completeness",
|
||||
"icon": "CheckCircle",
|
||||
"gradient": "from-primary/10 to-accent/10",
|
||||
"dataSource": {
|
||||
"type": "computed",
|
||||
"compute": "calculateCompletionScore"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "metric",
|
||||
|
||||
@@ -133,9 +133,11 @@
|
||||
"data": [
|
||||
{
|
||||
"id": "activeFile",
|
||||
"type": "computed",
|
||||
"dependencies": ["files", "activeFileId"],
|
||||
"compute": "context.files.find(f => f.id === context.activeFileId)"
|
||||
"type": "static",
|
||||
"expression": "data.files.0",
|
||||
"dependencies": [
|
||||
"files"
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
|
||||
@@ -35,27 +35,28 @@
|
||||
},
|
||||
{
|
||||
"id": "selectedBlueprint",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.find(b => b.id === data.selectedBlueprintId) || null; }",
|
||||
"dependencies": ["flaskConfig", "selectedBlueprintId"]
|
||||
"type": "static",
|
||||
"expression": "data.flaskConfig.blueprints.find(id === data.selectedBlueprintId)",
|
||||
"dependencies": [
|
||||
"flaskConfig",
|
||||
"selectedBlueprintId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blueprintCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => ((data.flaskConfig || {}).blueprints || []).length",
|
||||
"dependencies": ["flaskConfig"]
|
||||
"type": "static",
|
||||
"expression": "data.flaskConfig.blueprints.length",
|
||||
"dependencies": [
|
||||
"flaskConfig"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "endpointCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { const bp = data.selectedBlueprint; return bp ? (bp.endpoints || []).length : 0; }",
|
||||
"dependencies": ["selectedBlueprint"]
|
||||
},
|
||||
{
|
||||
"id": "totalEndpoints",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.reduce((sum, bp) => sum + (bp.endpoints || []).length, 0); }",
|
||||
"dependencies": ["flaskConfig"]
|
||||
"type": "static",
|
||||
"expression": "data.selectedBlueprint.endpoints.length",
|
||||
"dependencies": [
|
||||
"selectedBlueprint"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
},
|
||||
{
|
||||
"id": "selectedLambda",
|
||||
"type": "computed",
|
||||
"compute": "(data) => data.lambdas?.find(l => l.id === data.selectedLambdaId) || null",
|
||||
"dependencies": ["lambdas", "selectedLambdaId"]
|
||||
"type": "static",
|
||||
"expression": "data.lambdas.find(id === data.selectedLambdaId)",
|
||||
"dependencies": [
|
||||
"lambdas",
|
||||
"selectedLambdaId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lambdaCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => (data.lambdas || []).length",
|
||||
"dependencies": ["lambdas"]
|
||||
"type": "static",
|
||||
"expression": "data.lambdas.length",
|
||||
"dependencies": [
|
||||
"lambdas"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@@ -71,7 +76,9 @@
|
||||
"props": {
|
||||
"className": "text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent"
|
||||
},
|
||||
"children": ["Lambdas"]
|
||||
"children": [
|
||||
"Lambdas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Badge",
|
||||
@@ -133,7 +140,9 @@
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"children": ["Lambda list will be rendered here"]
|
||||
"children": [
|
||||
"Lambda list will be rendered here"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -159,14 +168,18 @@
|
||||
"props": {
|
||||
"className": "text-lg font-semibold mb-2"
|
||||
},
|
||||
"children": ["No Lambdas Yet"]
|
||||
"children": [
|
||||
"No Lambdas Yet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-sm text-muted-foreground mb-4"
|
||||
},
|
||||
"children": ["Create your first serverless function"]
|
||||
"children": [
|
||||
"Create your first serverless function"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -182,101 +195,106 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "lambda-selection-state",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center p-8"
|
||||
},
|
||||
"conditional": {
|
||||
"if": "selectedLambda"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"if": "selectedLambda != null",
|
||||
"then": {
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "max-w-6xl mx-auto w-full space-y-6"
|
||||
"className": "flex-1 flex items-center justify-center p-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between"
|
||||
"className": "max-w-6xl mx-auto w-full space-y-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "h1",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedLambda",
|
||||
"path": "name"
|
||||
"type": "div",
|
||||
"children": [
|
||||
{
|
||||
"type": "h1",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedLambda",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedLambda",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedLambda",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center p-8"
|
||||
},
|
||||
"conditional": {
|
||||
"if": "!selectedLambda"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
},
|
||||
"else": {
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center"
|
||||
"className": "flex-1 flex items-center justify-center p-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "icon",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"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"
|
||||
"className": "text-center"
|
||||
},
|
||||
"children": ["No Lambda Selected"]
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"children": ["Select a lambda from the sidebar or create a new one"]
|
||||
"children": [
|
||||
{
|
||||
"type": "icon",
|
||||
"props": {
|
||||
"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": [
|
||||
"No Lambda Selected"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"children": [
|
||||
"Select a lambda from the sidebar or create a new one"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,15 +28,20 @@
|
||||
},
|
||||
{
|
||||
"id": "selectedModel",
|
||||
"type": "computed",
|
||||
"compute": "(data) => data.models?.find(m => m.id === data.selectedModelId) || null",
|
||||
"dependencies": ["models", "selectedModelId"]
|
||||
"type": "static",
|
||||
"expression": "data.models.find(id === data.selectedModelId)",
|
||||
"dependencies": [
|
||||
"models",
|
||||
"selectedModelId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "modelCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => (data.models || []).length",
|
||||
"dependencies": ["models"]
|
||||
"type": "static",
|
||||
"expression": "data.models.length",
|
||||
"dependencies": [
|
||||
"models"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@@ -131,55 +136,142 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state",
|
||||
"id": "model-selection-state",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center"
|
||||
},
|
||||
"condition": {
|
||||
"source": "selectedModel",
|
||||
"transform": "(val) => !val"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-content",
|
||||
"conditional": {
|
||||
"if": "selectedModel != null",
|
||||
"then": {
|
||||
"id": "model-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center space-y-4"
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "Heading",
|
||||
"id": "model-header",
|
||||
"type": "div",
|
||||
"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"
|
||||
"className": "mb-6"
|
||||
},
|
||||
"events": [
|
||||
"children": [
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
"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": "open-create-from-empty",
|
||||
"type": "set-value",
|
||||
"target": "createDialogOpen",
|
||||
"value": true
|
||||
"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": {
|
||||
"className": "text-center space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-2xl font-bold text-muted-foreground",
|
||||
"children": "No Model Selected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-description",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground",
|
||||
"children": "Select a model from the sidebar or create a new one"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-button",
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "default",
|
||||
"children": "Create Your First Model"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"event": "click",
|
||||
"actions": [
|
||||
{
|
||||
"id": "open-create-from-empty",
|
||||
"type": "set-value",
|
||||
"target": "createDialogOpen",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -187,95 +279,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "model-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"condition": {
|
||||
"source": "selectedModel",
|
||||
"transform": "(val) => !!val"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "model-header",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "model-name",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold mb-2"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedModel",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "model-description",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedModel",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fields-card",
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"id": "fields-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "fields-title",
|
||||
"type": "CardTitle",
|
||||
"props": {
|
||||
"children": "Model Fields"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fields-description",
|
||||
"type": "CardDescription",
|
||||
"props": {
|
||||
"children": "Define the fields and their types for this model"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fields-content",
|
||||
"type": "CardContent",
|
||||
"children": [
|
||||
{
|
||||
"id": "fields-placeholder",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center text-muted-foreground py-8 border-2 border-dashed border-border rounded-lg",
|
||||
"children": "Add fields to define your data model"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,63 +5,15 @@
|
||||
"id": "lastSaved",
|
||||
"type": "static",
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"id": "currentTime",
|
||||
"type": "static",
|
||||
"defaultValue": 0,
|
||||
"polling": {
|
||||
"interval": 10000,
|
||||
"update": "() => Date.now()"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "isRecent",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { if (!data.lastSaved) return false; return Date.now() - data.lastSaved < 3000; }",
|
||||
"dependencies": ["lastSaved", "currentTime"]
|
||||
},
|
||||
{
|
||||
"id": "timeAgo",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { if (!data.lastSaved) return ''; const seconds = Math.floor((Date.now() - data.lastSaved) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; return `${Math.floor(seconds / 86400)}d ago`; }",
|
||||
"dependencies": ["lastSaved", "currentTime"]
|
||||
}
|
||||
],
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||
},
|
||||
"type": "SaveIndicator",
|
||||
"conditional": {
|
||||
"if": "lastSaved !== null"
|
||||
"if": "lastSaved != null"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"bindings": {
|
||||
"lastSaved": {
|
||||
"source": "lastSaved"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,21 +54,27 @@
|
||||
},
|
||||
{
|
||||
"id": "activeVariant",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { const theme = data.theme || {}; const variants = theme.variants || []; return variants.find(v => v.id === theme.activeVariantId) || variants[0] || null; }",
|
||||
"dependencies": ["theme"]
|
||||
"type": "static",
|
||||
"expression": "data.theme.variants.find(id === data.theme.activeVariantId)",
|
||||
"dependencies": [
|
||||
"theme"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "variantCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => ((data.theme || {}).variants || []).length",
|
||||
"dependencies": ["theme"]
|
||||
"type": "static",
|
||||
"expression": "data.theme.variants.length",
|
||||
"dependencies": [
|
||||
"theme"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "customColorCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => { const variant = data.activeVariant; if (!variant || !variant.colors) return 0; return Object.keys(variant.colors.customColors || {}).length; }",
|
||||
"dependencies": ["activeVariant"]
|
||||
"type": "static",
|
||||
"expression": "Object.keys(data.activeVariant.colors.customColors).length",
|
||||
"dependencies": [
|
||||
"activeVariant"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -32,15 +32,20 @@
|
||||
},
|
||||
{
|
||||
"id": "selectedWorkflow",
|
||||
"type": "computed",
|
||||
"compute": "(data) => data.workflows?.find(w => w.id === data.selectedWorkflowId) || null",
|
||||
"dependencies": ["workflows", "selectedWorkflowId"]
|
||||
"type": "static",
|
||||
"expression": "data.workflows.find(id === data.selectedWorkflowId)",
|
||||
"dependencies": [
|
||||
"workflows",
|
||||
"selectedWorkflowId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workflowCount",
|
||||
"type": "computed",
|
||||
"compute": "(data) => (data.workflows || []).length",
|
||||
"dependencies": ["workflows"]
|
||||
"type": "static",
|
||||
"expression": "data.workflows.length",
|
||||
"dependencies": [
|
||||
"workflows"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@@ -71,7 +76,9 @@
|
||||
"props": {
|
||||
"className": "text-xl font-bold mb-2 flex items-center gap-2"
|
||||
},
|
||||
"children": ["Workflows"]
|
||||
"children": [
|
||||
"Workflows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "create-button",
|
||||
@@ -117,7 +124,9 @@
|
||||
"props": {
|
||||
"className": "text-sm text-muted-foreground"
|
||||
},
|
||||
"children": ["Status Filter"]
|
||||
"children": [
|
||||
"Status Filter"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -136,7 +145,9 @@
|
||||
"props": {
|
||||
"className": "text-center py-8 text-muted-foreground"
|
||||
},
|
||||
"children": ["No workflows yet"]
|
||||
"children": [
|
||||
"No workflows yet"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -150,122 +161,129 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state",
|
||||
"id": "workflow-selection-state",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center"
|
||||
},
|
||||
"conditional": {
|
||||
"if": "!selectedWorkflow"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-content",
|
||||
"if": "selectedWorkflow != null",
|
||||
"then": {
|
||||
"id": "workflow-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center space-y-4"
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "icon",
|
||||
"id": "workflow-header",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"name": "GitBranch",
|
||||
"className": "h-20 w-20 text-muted-foreground/50 mx-auto",
|
||||
"weight": "duotone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "h3",
|
||||
"props": {
|
||||
"className": "text-2xl font-bold text-muted-foreground"
|
||||
},
|
||||
"children": ["No Workflow Selected"]
|
||||
},
|
||||
{
|
||||
"id": "empty-state-description",
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"children": ["Select a workflow from the sidebar or create a new one"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workflow-editor",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 p-6 overflow-auto"
|
||||
},
|
||||
"conditional": {
|
||||
"if": "selectedWorkflow"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "workflow-header",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "workflow-name",
|
||||
"type": "h1",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold mb-2"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedWorkflow",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workflow-description",
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedWorkflow",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workflow-canvas",
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"className": "min-h-[400px] bg-muted/20"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-content",
|
||||
"type": "CardContent",
|
||||
"props": {
|
||||
"className": "p-6"
|
||||
"className": "mb-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-placeholder",
|
||||
"type": "div",
|
||||
"id": "workflow-name",
|
||||
"type": "h1",
|
||||
"props": {
|
||||
"className": "text-center text-muted-foreground py-12"
|
||||
"className": "text-3xl font-bold mb-2"
|
||||
},
|
||||
"children": ["Workflow canvas - Add nodes to build your workflow"]
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedWorkflow",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workflow-description",
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "selectedWorkflow",
|
||||
"path": "description"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "workflow-canvas",
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"className": "min-h-[400px] bg-muted/20"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-content",
|
||||
"type": "CardContent",
|
||||
"props": {
|
||||
"className": "p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-placeholder",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center text-muted-foreground py-12"
|
||||
},
|
||||
"children": [
|
||||
"Workflow canvas - Add nodes to build your workflow"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"else": {
|
||||
"id": "empty-state",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex-1 flex items-center justify-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state-content",
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "text-center space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "icon",
|
||||
"props": {
|
||||
"name": "GitBranch",
|
||||
"className": "h-20 w-20 text-muted-foreground/50 mx-auto",
|
||||
"weight": "duotone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "empty-state-title",
|
||||
"type": "h3",
|
||||
"props": {
|
||||
"className": "text-2xl font-bold text-muted-foreground"
|
||||
},
|
||||
"children": [
|
||||
"No Workflow Selected"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "empty-state-description",
|
||||
"type": "p",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"children": [
|
||||
"Select a workflow from the sidebar or create a new one"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Data Binding Designer",
|
||||
"description": "Connect UI components to KV storage and computed values"
|
||||
"description": "Connect UI components to KV storage and static data"
|
||||
},
|
||||
"bindingsCard": {
|
||||
"title": "Component Bindings",
|
||||
@@ -13,7 +13,6 @@
|
||||
"title": "How It Works",
|
||||
"steps": [
|
||||
"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"
|
||||
]
|
||||
},
|
||||
@@ -34,12 +33,6 @@
|
||||
"key": "app-counter",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"type": "computed",
|
||||
"dependencies": ["userProfile"],
|
||||
"expression": "data.userProfile.name"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
@@ -50,7 +43,8 @@
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "displayName"
|
||||
"source": "userProfile",
|
||||
"path": "name"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Edit Data Source",
|
||||
"description": "Configure the data source settings and dependencies",
|
||||
"description": "Configure the data source settings",
|
||||
"fields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
@@ -18,17 +18,6 @@
|
||||
"valueLabel": "Value (JSON)",
|
||||
"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": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Changes"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Data Sources",
|
||||
"description": "Manage KV storage, computed values, and static data"
|
||||
"description": "Manage KV storage and static data"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add Data Source"
|
||||
},
|
||||
"menu": {
|
||||
"kv": "KV Store",
|
||||
"computed": "Computed Value",
|
||||
"static": "Static Data"
|
||||
},
|
||||
"emptyState": {
|
||||
@@ -17,12 +16,11 @@
|
||||
},
|
||||
"groups": {
|
||||
"kv": "KV Store",
|
||||
"static": "Static Data",
|
||||
"computed": "Computed Values"
|
||||
"static": "Static Data"
|
||||
},
|
||||
"toasts": {
|
||||
"deleteBlockedTitle": "Cannot delete",
|
||||
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
|
||||
"deleteBlockedDescription": "This source is used by {count} dependent {noun}",
|
||||
"deleted": "Data source deleted",
|
||||
"updated": "Data source updated"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
|
||||
export function useDataSourceEditor(
|
||||
dataSource: DataSource | null,
|
||||
allDataSources: DataSource[],
|
||||
) {
|
||||
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
||||
|
||||
@@ -15,44 +14,8 @@ export function useDataSourceEditor(
|
||||
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 {
|
||||
editingSource,
|
||||
updateField,
|
||||
addDependency,
|
||||
removeDependency,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
||||
id: `ds-${Date.now()}`,
|
||||
type,
|
||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||
...(type === 'computed' && { expression: '', dependencies: [] }),
|
||||
...(type === 'static' && { defaultValue: null }),
|
||||
}
|
||||
|
||||
@@ -33,7 +32,6 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
||||
|
||||
const getDependents = useCallback((sourceId: string) => {
|
||||
return dataSources.filter(ds =>
|
||||
ds.type === 'computed' &&
|
||||
ds.dependencies?.includes(sourceId)
|
||||
)
|
||||
}, [dataSources])
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useKV } from '@/hooks/use-kv'
|
||||
|
||||
export type DataSourceType = 'kv' | 'static' | 'computed'
|
||||
export type DataSourceType = 'kv' | 'static'
|
||||
|
||||
export interface DataSourceConfig<T = any> {
|
||||
type: DataSourceType
|
||||
key?: string
|
||||
defaultValue?: T
|
||||
compute?: (allData: Record<string, any>) => T
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
export function useKVDataSource<T = any>(key: string, defaultValue?: T) {
|
||||
@@ -18,13 +16,6 @@ export function useStaticDataSource<T = any>(defaultValue: T) {
|
||||
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[]) {
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -41,20 +41,20 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
||||
|
||||
computedSources.forEach(source => {
|
||||
const derivedSources = dataSources.filter(ds => ds.expression || ds.valueTemplate)
|
||||
|
||||
derivedSources.forEach(source => {
|
||||
const deps = source.dependencies || []
|
||||
const hasAllDeps = deps.every(dep => dep in data)
|
||||
|
||||
if (hasAllDeps) {
|
||||
const evaluationContext = { data }
|
||||
const computedValue = source.expression
|
||||
const derivedValue = source.expression
|
||||
? evaluateExpression(source.expression, evaluationContext)
|
||||
: source.valueTemplate
|
||||
? evaluateTemplate(source.valueTemplate, evaluationContext)
|
||||
: source.defaultValue
|
||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
||||
setData(prev => ({ ...prev, [source.id]: derivedValue }))
|
||||
}
|
||||
})
|
||||
}, [data, dataSources])
|
||||
|
||||
@@ -13,8 +13,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
[dataSources]
|
||||
)
|
||||
|
||||
const computedSources = useMemo(
|
||||
() => dataSources.filter((ds) => ds.type === 'computed'),
|
||||
const derivedSources = useMemo(
|
||||
() => dataSources.filter((ds) => ds.expression || ds.valueTemplate),
|
||||
[dataSources]
|
||||
)
|
||||
|
||||
@@ -54,8 +54,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
const computedData = useMemo(() => {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
computedSources.forEach((ds) => {
|
||||
const evaluationContext = { data }
|
||||
derivedSources.forEach((ds) => {
|
||||
const evaluationContext = { data: { ...data, ...result } }
|
||||
if (ds.expression) {
|
||||
result[ds.id] = evaluateExpression(ds.expression, evaluationContext)
|
||||
return
|
||||
@@ -70,7 +70,7 @@ export function useDataSources(dataSources: DataSource[]) {
|
||||
})
|
||||
|
||||
return result
|
||||
}, [computedSources, data])
|
||||
}, [derivedSources, data])
|
||||
|
||||
const allData = useMemo(
|
||||
() => ({ ...data, ...computedData }),
|
||||
|
||||
@@ -45,22 +45,26 @@ export function usePage(schema: PageSchema) {
|
||||
useEffect(() => {
|
||||
if (schema.data) {
|
||||
const computed: Record<string, any> = {}
|
||||
|
||||
|
||||
schema.data.forEach(source => {
|
||||
if (source.type === 'computed') {
|
||||
if (source.expression) {
|
||||
computed[source.id] = evaluateBindingExpression(source.expression, dataContext, {
|
||||
fallback: undefined,
|
||||
label: `computed data (${source.id})`,
|
||||
})
|
||||
} else if (source.valueTemplate) {
|
||||
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: dataContext })
|
||||
}
|
||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||
if (source.expression) {
|
||||
computed[source.id] = evaluateBindingExpression(source.expression, { ...dataContext, ...computed }, {
|
||||
fallback: undefined,
|
||||
label: `derived data (${source.id})`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (source.valueTemplate) {
|
||||
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: { ...dataContext, ...computed } })
|
||||
return
|
||||
}
|
||||
|
||||
if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||
computed[source.id] = source.defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
setComputedData(computed)
|
||||
}
|
||||
}, [schema.data, dataContext])
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
|
||||
interface UseDataSourceEditorParams {
|
||||
dataSource: DataSource | null
|
||||
allDataSources: DataSource[]
|
||||
onSave: (dataSource: DataSource) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function useDataSourceEditor({
|
||||
dataSource,
|
||||
allDataSources,
|
||||
onSave,
|
||||
onOpenChange,
|
||||
}: UseDataSourceEditorParams) {
|
||||
@@ -27,51 +25,15 @@ export function useDataSourceEditor({
|
||||
})
|
||||
}, [])
|
||||
|
||||
const addDependency = useCallback((depId: string) => {
|
||||
setEditingSource((prev) => {
|
||||
if (!prev || prev.type !== 'computed') return prev
|
||||
const deps = prev.dependencies || []
|
||||
if (deps.includes(depId)) return prev
|
||||
return { ...prev, dependencies: [...deps, depId] }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const removeDependency = useCallback((depId: string) => {
|
||||
setEditingSource((prev) => {
|
||||
if (!prev || prev.type !== 'computed') return prev
|
||||
const deps = prev.dependencies || []
|
||||
return { ...prev, dependencies: deps.filter((id) => id !== depId) }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!editingSource) return
|
||||
onSave(editingSource)
|
||||
onOpenChange(false)
|
||||
}, [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 {
|
||||
editingSource,
|
||||
updateField,
|
||||
addDependency,
|
||||
removeDependency,
|
||||
handleSave,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,35 @@ export function evaluateExpression(
|
||||
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"
|
||||
if (expression.startsWith('data.')) {
|
||||
return getNestedValue(data, expression.substring(5))
|
||||
|
||||
@@ -8,7 +8,6 @@ export function useJSONDataSource<T = unknown>(
|
||||
) {
|
||||
const kvConfig = config.type === 'kv' ? config.config : undefined
|
||||
const apiConfig = config.type === 'api' ? config.config : undefined
|
||||
const computedConfig = config.type === 'computed' ? config.config : undefined
|
||||
const defaultValue =
|
||||
config.type === 'static' ? config.config : config.config?.defaultValue
|
||||
|
||||
@@ -57,8 +56,6 @@ export function useJSONDataSource<T = unknown>(
|
||||
return apiValue
|
||||
case 'static':
|
||||
return config.config
|
||||
case 'computed':
|
||||
return computedConfig?.defaultValue
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export const PageUISchema = z.object({
|
||||
tables: z.array(TableSchema).optional(),
|
||||
menus: z.array(MenuSchema).optional(),
|
||||
dataSources: z.record(z.string(), z.object({
|
||||
type: z.enum(['kv', 'api', 'computed', 'static']),
|
||||
type: z.enum(['kv', 'api', 'static']),
|
||||
config: z.any(),
|
||||
})).optional(),
|
||||
})
|
||||
@@ -241,13 +241,6 @@ export type DataSourceConfig<T = unknown> =
|
||||
transform?: (data: unknown) => T
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'computed'
|
||||
config: {
|
||||
defaultValue?: T
|
||||
transform?: (data: unknown) => T
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'static'
|
||||
config: T
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import { StatusIcon } from '@/components/atoms'
|
||||
import { useSaveIndicator } from '@/hooks/use-save-indicator'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SaveIndicatorWrapperProps } from './interfaces'
|
||||
|
||||
export function SaveIndicatorWrapper({
|
||||
lastSaved,
|
||||
status = 'saved',
|
||||
label,
|
||||
showLabel = true,
|
||||
animate,
|
||||
className,
|
||||
}: 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 shouldAnimate = animate ?? status === 'saved'
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { UIComponent } from '@/types/json-ui'
|
||||
export type SaveIndicatorStatus = 'saved' | 'synced'
|
||||
|
||||
export interface SaveIndicatorWrapperProps {
|
||||
lastSaved?: number | null
|
||||
status?: SaveIndicatorStatus
|
||||
label?: string
|
||||
showLabel?: boolean
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
},
|
||||
{
|
||||
"id": "filteredUsers",
|
||||
"type": "computed",
|
||||
"type": "static",
|
||||
"expression": "data.users",
|
||||
"dependencies": ["users", "filterQuery"]
|
||||
},
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"type": "static",
|
||||
"valueTemplate": {
|
||||
"total": "data.users.length",
|
||||
"active": "data.users.filter(status === 'active').length",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
{
|
||||
"id": "stats",
|
||||
"type": "computed",
|
||||
"type": "static",
|
||||
"valueTemplate": {
|
||||
"total": "data.todos.length",
|
||||
"completed": "data.todos.filter(completed === true).length",
|
||||
|
||||
@@ -42,7 +42,7 @@ export type ActionType =
|
||||
| 'custom'
|
||||
|
||||
export type DataSourceType =
|
||||
| 'kv' | 'computed' | 'static'
|
||||
| 'kv' | 'static'
|
||||
|
||||
export type BindingSourceType =
|
||||
| 'data' | 'bindings' | 'state'
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ComponentSchema: z.ZodType<any> = z.lazy(() =>
|
||||
|
||||
export const DataSourceSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(['kv', 'computed', 'static', 'ai'], { message: 'Invalid data source type' }),
|
||||
type: z.enum(['kv', 'static', 'ai'], { message: 'Invalid data source type' }),
|
||||
key: z.string().optional(),
|
||||
defaultValue: z.any().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
|
||||
Reference in New Issue
Block a user