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:
2026-01-18 18:33:08 +00:00
committed by GitHub
40 changed files with 653 additions and 903 deletions

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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 }}
/>
)
}

View File

@@ -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',

View File

@@ -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)}>

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>
</>
)
}

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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)
}
})

View File

@@ -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(),

View File

@@ -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": []
}
}

View File

@@ -6,10 +6,6 @@
"title": "Project Completeness",
"icon": "CheckCircle",
"gradient": "from-primary/10 to-accent/10",
"dataSource": {
"type": "computed",
"compute": "calculateCompletionScore"
},
"components": [
{
"type": "metric",

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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"
]
}
]
}
]
}
]
}
}
]
}

View File

@@ -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"
}
}
]
}
]
}
]
}
}
]
}

View File

@@ -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"
}
]
}
}

View File

@@ -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": [

View File

@@ -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"
]
}
]
}
]
}
]
}
}
]
}

View File

@@ -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"
}
}
},

View File

@@ -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"

View File

@@ -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"
}

View File

@@ -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,
}
}

View File

@@ -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])

View File

@@ -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 {}
}

View File

@@ -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])

View File

@@ -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 }),

View File

@@ -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])

View File

@@ -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,
}
}

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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",

View File

@@ -22,7 +22,7 @@
},
{
"id": "stats",
"type": "computed",
"type": "static",
"valueTemplate": {
"total": "data.todos.length",
"completed": "data.todos.filter(completed === true).length",

View File

@@ -42,7 +42,7 @@ export type ActionType =
| 'custom'
export type DataSourceType =
| 'kv' | 'computed' | 'static'
| 'kv' | 'static'
export type BindingSourceType =
| 'data' | 'bindings' | 'state'

View File

@@ -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(),