mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 22:25:01 +00:00
Compare commits
1 Commits
copilot/re
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
| b57c4014ca |
@@ -7,30 +7,9 @@ import { ComponentBindingsCard } from '@/components/data-binding-designer/Compon
|
|||||||
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
||||||
import dataBindingCopy from '@/data/data-binding-designer.json'
|
import dataBindingCopy from '@/data/data-binding-designer.json'
|
||||||
|
|
||||||
interface SeedDataSource extends Omit<DataSource, 'compute'> {
|
|
||||||
computeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeRegistry: Record<string, (data: Record<string, any>) => any> = {
|
|
||||||
displayName: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildSeedDataSources = (sources: SeedDataSource[]): DataSource[] => {
|
|
||||||
return sources.map((source) => {
|
|
||||||
if (source.type === 'computed' && source.computeId) {
|
|
||||||
return {
|
|
||||||
...source,
|
|
||||||
compute: computeRegistry[source.computeId],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return source
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataBindingDesigner() {
|
export function DataBindingDesigner() {
|
||||||
const [dataSources, setDataSources] = useState<DataSource[]>(
|
const [dataSources, setDataSources] = useState<DataSource[]>(
|
||||||
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
|
dataBindingCopy.seed.dataSources as DataSource[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
||||||
|
|||||||
@@ -45,11 +45,12 @@ function getCompletionMessage(score: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectDashboard(props: ProjectDashboardProps) {
|
export function ProjectDashboard(props: ProjectDashboardProps) {
|
||||||
|
const completionSummary = calculateCompletionScore(props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JSONPageRenderer
|
<JSONPageRenderer
|
||||||
schema={dashboardSchema as any}
|
schema={dashboardSchema as any}
|
||||||
data={props}
|
data={{ ...props, completionSummary }}
|
||||||
functions={{ calculateCompletionScore }}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
import { DataSourceType } from '@/types/json-ui'
|
||||||
import { Database, Function, File } from '@phosphor-icons/react'
|
import { Database, File } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceBadgeProps {
|
interface DataSourceBadgeProps {
|
||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
@@ -13,11 +13,6 @@ const dataSourceConfig = {
|
|||||||
label: 'KV Storage',
|
label: 'KV Storage',
|
||||||
className: 'bg-accent/20 text-accent border-accent/30'
|
className: 'bg-accent/20 text-accent border-accent/30'
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
icon: Function,
|
|
||||||
label: 'Computed',
|
|
||||||
className: 'bg-primary/20 text-primary border-primary/30'
|
|
||||||
},
|
|
||||||
static: {
|
static: {
|
||||||
icon: File,
|
icon: File,
|
||||||
label: 'Static',
|
label: 'Static',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Progress } from '@/components/ui/progress'
|
|||||||
import { StatCard } from '@/components/atoms'
|
import { StatCard } from '@/components/atoms'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { getIcon, resolveBinding } from './utils'
|
import { getIcon, resolveBinding } from './utils'
|
||||||
|
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||||
|
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
import { LegacyPageSchema, PageSectionConfig } from './types'
|
import { LegacyPageSchema, PageSectionConfig } from './types'
|
||||||
|
|
||||||
interface PageSectionRendererProps {
|
interface PageSectionRendererProps {
|
||||||
@@ -107,8 +109,21 @@ function PageCard({ card, data, functions }: PageCardProps) {
|
|||||||
const icon = card.icon ? getIcon(card.icon) : null
|
const icon = card.icon ? getIcon(card.icon) : null
|
||||||
|
|
||||||
if (card.type === 'gradient-card') {
|
if (card.type === 'gradient-card') {
|
||||||
const computeFn = functions[card.dataSource?.compute]
|
const dataSource = card.dataSource
|
||||||
const computedData = computeFn ? computeFn(data) : {}
|
let computedData: Record<string, any> = {}
|
||||||
|
|
||||||
|
if (dataSource?.expression) {
|
||||||
|
const resolved = evaluateBindingExpression(dataSource.expression, data, {
|
||||||
|
fallback: {},
|
||||||
|
label: `dashboard card (${card.id})`,
|
||||||
|
})
|
||||||
|
computedData = resolved || {}
|
||||||
|
} else if (dataSource?.valueTemplate) {
|
||||||
|
computedData = evaluateTemplate(dataSource.valueTemplate, { data })
|
||||||
|
} else if (dataSource?.compute) {
|
||||||
|
const computeFn = functions[dataSource.compute]
|
||||||
|
computedData = computeFn ? computeFn(data) : {}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import { Card, Badge, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||||
|
|
||||||
interface DataSourceCardProps {
|
interface DataSourceCardProps {
|
||||||
dataSource: DataSource
|
dataSource: DataSource
|
||||||
dependents?: DataSource[]
|
|
||||||
onEdit: (id: string) => void
|
onEdit: (id: string) => void
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
export function DataSourceCard({ dataSource, onEdit, onDelete }: DataSourceCardProps) {
|
||||||
const getDependencyCount = () => {
|
|
||||||
if (dataSource.type === 'computed') {
|
|
||||||
return dataSource.dependencies?.length || 0
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderTypeSpecificInfo = () => {
|
const renderTypeSpecificInfo = () => {
|
||||||
if (dataSource.type === 'kv') {
|
if (dataSource.type === 'kv') {
|
||||||
return (
|
return (
|
||||||
@@ -27,18 +19,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataSource.type === 'computed') {
|
|
||||||
const depCount = getDependencyCount()
|
|
||||||
return (
|
|
||||||
<Flex align="center" gap="sm">
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
<ArrowsDownUp className="w-3 h-3 mr-1" />
|
|
||||||
{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +36,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
|||||||
|
|
||||||
{renderTypeSpecificInfo()}
|
{renderTypeSpecificInfo()}
|
||||||
|
|
||||||
{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'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Flex align="center" gap="xs">
|
<Flex align="center" gap="xs">
|
||||||
@@ -78,7 +51,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onDelete(dataSource.id)}
|
onClick={() => onDelete(dataSource.id)}
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
disabled={dependents.length > 0}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
|||||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||||
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
||||||
import { ComputedSourceFields } from '@/components/molecules/data-source-editor/ComputedSourceFields'
|
|
||||||
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
||||||
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||||
|
|
||||||
interface DataSourceEditorDialogProps {
|
interface DataSourceEditorDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
dataSource: DataSource | null
|
dataSource: DataSource | null
|
||||||
allDataSources: DataSource[]
|
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onSave: (dataSource: DataSource) => void
|
onSave: (dataSource: DataSource) => void
|
||||||
}
|
}
|
||||||
@@ -20,19 +18,13 @@ interface DataSourceEditorDialogProps {
|
|||||||
export function DataSourceEditorDialog({
|
export function DataSourceEditorDialog({
|
||||||
open,
|
open,
|
||||||
dataSource,
|
dataSource,
|
||||||
allDataSources,
|
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSave,
|
onSave,
|
||||||
}: DataSourceEditorDialogProps) {
|
}: DataSourceEditorDialogProps) {
|
||||||
const {
|
const {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
} = useDataSourceEditor(dataSource)
|
||||||
removeDependency,
|
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
} = useDataSourceEditor(dataSource, allDataSources)
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!editingSource) return
|
if (!editingSource) return
|
||||||
@@ -80,18 +72,6 @@ export function DataSourceEditorDialog({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editingSource.type === 'computed' && (
|
|
||||||
<ComputedSourceFields
|
|
||||||
editingSource={editingSource}
|
|
||||||
availableDeps={availableDeps}
|
|
||||||
selectedDeps={selectedDeps}
|
|
||||||
unselectedDeps={unselectedDeps}
|
|
||||||
copy={dataSourceEditorCopy.computed}
|
|
||||||
onUpdateField={updateField}
|
|
||||||
onAddDependency={addDependency}
|
|
||||||
onRemoveDependency={removeDependency}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -1,110 +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 {
|
|
||||||
computeLabel: string
|
|
||||||
computePlaceholder: string
|
|
||||||
computeHelp: 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.computeLabel}</Label>
|
|
||||||
<Textarea
|
|
||||||
value={editingSource.compute?.toString() || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
const fn = new Function('data', `return (${e.target.value})`)()
|
|
||||||
onUpdateField('compute', fn)
|
|
||||||
} catch (err) {
|
|
||||||
// Invalid function
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={copy.computePlaceholder}
|
|
||||||
className="font-mono text-sm h-24"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{copy.computeHelp}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>{copy.dependenciesLabel}</Label>
|
|
||||||
|
|
||||||
{selectedDeps.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
|
|
||||||
{selectedDeps.map(depId => (
|
|
||||||
<Badge
|
|
||||||
key={depId}
|
|
||||||
variant="secondary"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{depId}
|
|
||||||
<button
|
|
||||||
onClick={() => onRemoveDependency(depId)}
|
|
||||||
className="ml-1 hover:text-destructive"
|
|
||||||
>
|
|
||||||
<X className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unselectedDeps.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs text-muted-foreground">{copy.availableSourcesLabel}</Label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{unselectedDeps.map(ds => (
|
|
||||||
<Button
|
|
||||||
key={ds.id}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onAddDependency(ds.id)}
|
|
||||||
className="h-7 text-xs"
|
|
||||||
>
|
|
||||||
+ {ds.id}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{availableDeps.length === 0 && selectedDeps.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{copy.emptyDependencies}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|||||||
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
||||||
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
||||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||||
import { Database, Function, FileText } from '@phosphor-icons/react'
|
import { Database, FileText } from '@phosphor-icons/react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { EmptyState, Stack } from '@/components/atoms'
|
import { EmptyState, Stack } from '@/components/atoms'
|
||||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||||
@@ -21,7 +21,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
addDataSource,
|
addDataSource,
|
||||||
updateDataSource,
|
updateDataSource,
|
||||||
deleteDataSource,
|
deleteDataSource,
|
||||||
getDependents,
|
|
||||||
} = useDataSourceManager(dataSources)
|
} = useDataSourceManager(dataSources)
|
||||||
|
|
||||||
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
|
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
|
||||||
@@ -42,17 +41,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteSource = (id: string) => {
|
const handleDeleteSource = (id: string) => {
|
||||||
const dependents = getDependents(id)
|
|
||||||
if (dependents.length > 0) {
|
|
||||||
const noun = dependents.length === 1 ? 'source' : 'sources'
|
|
||||||
toast.error(dataSourceManagerCopy.toasts.deleteBlockedTitle, {
|
|
||||||
description: dataSourceManagerCopy.toasts.deleteBlockedDescription
|
|
||||||
.replace('{count}', String(dependents.length))
|
|
||||||
.replace('{noun}', noun),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteDataSource(id)
|
deleteDataSource(id)
|
||||||
onChange(localSources.filter(ds => ds.id !== id))
|
onChange(localSources.filter(ds => ds.id !== id))
|
||||||
toast.success(dataSourceManagerCopy.toasts.deleted)
|
toast.success(dataSourceManagerCopy.toasts.deleted)
|
||||||
@@ -66,7 +54,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
|
|
||||||
const groupedSources = {
|
const groupedSources = {
|
||||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||||
computed: localSources.filter(ds => ds.type === 'computed'),
|
|
||||||
static: localSources.filter(ds => ds.type === 'static'),
|
static: localSources.filter(ds => ds.type === 'static'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +84,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
icon={<Database size={16} />}
|
icon={<Database size={16} />}
|
||||||
label={dataSourceManagerCopy.groups.kv}
|
label={dataSourceManagerCopy.groups.kv}
|
||||||
dataSources={groupedSources.kv}
|
dataSources={groupedSources.kv}
|
||||||
getDependents={getDependents}
|
|
||||||
onEdit={handleEditSource}
|
onEdit={handleEditSource}
|
||||||
onDelete={handleDeleteSource}
|
onDelete={handleDeleteSource}
|
||||||
/>
|
/>
|
||||||
@@ -106,16 +92,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
icon={<FileText size={16} />}
|
icon={<FileText size={16} />}
|
||||||
label={dataSourceManagerCopy.groups.static}
|
label={dataSourceManagerCopy.groups.static}
|
||||||
dataSources={groupedSources.static}
|
dataSources={groupedSources.static}
|
||||||
getDependents={getDependents}
|
|
||||||
onEdit={handleEditSource}
|
|
||||||
onDelete={handleDeleteSource}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataSourceGroupSection
|
|
||||||
icon={<Function size={16} />}
|
|
||||||
label={dataSourceManagerCopy.groups.computed}
|
|
||||||
dataSources={groupedSources.computed}
|
|
||||||
getDependents={getDependents}
|
|
||||||
onEdit={handleEditSource}
|
onEdit={handleEditSource}
|
||||||
onDelete={handleDeleteSource}
|
onDelete={handleDeleteSource}
|
||||||
/>
|
/>
|
||||||
@@ -127,7 +103,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
|||||||
<DataSourceEditorDialog
|
<DataSourceEditorDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
dataSource={editingSource}
|
dataSource={editingSource}
|
||||||
allDataSources={localSources}
|
|
||||||
onOpenChange={setDialogOpen}
|
onOpenChange={setDialogOpen}
|
||||||
onSave={handleSaveSource}
|
onSave={handleSaveSource}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ interface DataSourceGroupSectionProps {
|
|||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
label: string
|
label: string
|
||||||
dataSources: DataSource[]
|
dataSources: DataSource[]
|
||||||
getDependents: (id: string) => string[]
|
|
||||||
onEdit: (id: string) => void
|
onEdit: (id: string) => void
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
}
|
}
|
||||||
@@ -16,7 +15,6 @@ export function DataSourceGroupSection({
|
|||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
dataSources,
|
dataSources,
|
||||||
getDependents,
|
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: DataSourceGroupSectionProps) {
|
}: DataSourceGroupSectionProps) {
|
||||||
@@ -37,7 +35,6 @@ export function DataSourceGroupSection({
|
|||||||
<DataSourceCard
|
<DataSourceCard
|
||||||
key={ds.id}
|
key={ds.id}
|
||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
dependents={getDependents(ds.id)}
|
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||||
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
import { Plus, Database, FileText } from '@phosphor-icons/react'
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
import { DataSourceType } from '@/types/json-ui'
|
||||||
|
|
||||||
interface DataSourceManagerHeaderCopy {
|
interface DataSourceManagerHeaderCopy {
|
||||||
@@ -14,7 +14,6 @@ interface DataSourceManagerHeaderCopy {
|
|||||||
addLabel: string
|
addLabel: string
|
||||||
menu: {
|
menu: {
|
||||||
kv: string
|
kv: string
|
||||||
computed: string
|
|
||||||
static: string
|
static: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,10 +48,6 @@ export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeader
|
|||||||
<Database className="w-4 h-4 mr-2" />
|
<Database className="w-4 h-4 mr-2" />
|
||||||
{copy.menu.kv}
|
{copy.menu.kv}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onAdd('computed')}>
|
|
||||||
<Function className="w-4 h-4 mr-2" />
|
|
||||||
{copy.menu.computed}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => onAdd('static')}>
|
<DropdownMenuItem onClick={() => onAdd('static')}>
|
||||||
<FileText className="w-4 h-4 mr-2" />
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
{copy.menu.static}
|
{copy.menu.static}
|
||||||
|
|||||||
@@ -37,13 +37,6 @@ export function useDataSource(source: DataSource) {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
}
|
}
|
||||||
case 'computed':
|
|
||||||
return {
|
|
||||||
data: source.defaultValue,
|
|
||||||
setData: () => {},
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
@@ -67,7 +60,7 @@ export function useDataSources(sources: DataSource[]) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sources.forEach((source) => {
|
sources.forEach((source) => {
|
||||||
if (source.type === 'static' || source.type === 'computed') {
|
if (source.type === 'static') {
|
||||||
updateData(source.id, source.defaultValue)
|
updateData(source.id, source.defaultValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const ActionSchema = z.object({
|
|||||||
|
|
||||||
export const DataSourceSchema = z.object({
|
export const DataSourceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.enum(['kv', 'api', 'computed', 'static'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'api', 'static'], { message: 'Invalid data source type' }),
|
||||||
key: z.string().optional(),
|
key: z.string().optional(),
|
||||||
endpoint: z.string().optional(),
|
endpoint: z.string().optional(),
|
||||||
transform: z.string().optional(),
|
transform: z.string().optional(),
|
||||||
|
|||||||
@@ -33,15 +33,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedTree",
|
"id": "selectedTree",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.trees?.find(t => t.id === data.selectedTreeId) || null",
|
"expression": "findById(data.trees, data.selectedTreeId)",
|
||||||
"dependencies": ["trees", "selectedTreeId"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "treeCount",
|
"id": "treeCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.trees || []).length",
|
"expression": "length(data.trees)",
|
||||||
"dependencies": ["trees"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
"icon": "CheckCircle",
|
"icon": "CheckCircle",
|
||||||
"gradient": "from-primary/10 to-accent/10",
|
"gradient": "from-primary/10 to-accent/10",
|
||||||
"dataSource": {
|
"dataSource": {
|
||||||
"type": "computed",
|
"type": "expression",
|
||||||
"compute": "calculateCompletionScore"
|
"expression": "data.completionSummary"
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,9 +133,9 @@
|
|||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": "activeFile",
|
"id": "activeFile",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"dependencies": ["files", "activeFileId"],
|
"expression": "findById(data.files, data.activeFileId)",
|
||||||
"compute": "context.files.find(f => f.id === context.activeFileId)"
|
"defaultValue": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
|
|||||||
@@ -35,27 +35,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedBlueprint",
|
"id": "selectedBlueprint",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.find(b => b.id === data.selectedBlueprintId) || null; }",
|
"expression": "findById(data.flaskConfig.blueprints, data.selectedBlueprintId)",
|
||||||
"dependencies": ["flaskConfig", "selectedBlueprintId"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "blueprintCount",
|
"id": "blueprintCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => ((data.flaskConfig || {}).blueprints || []).length",
|
"expression": "length(data.flaskConfig.blueprints)",
|
||||||
"dependencies": ["flaskConfig"]
|
"defaultValue": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "endpointCount",
|
"id": "endpointCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const bp = data.selectedBlueprint; return bp ? (bp.endpoints || []).length : 0; }",
|
"expression": "length(data.selectedBlueprint.endpoints)",
|
||||||
"dependencies": ["selectedBlueprint"]
|
"defaultValue": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "totalEndpoints",
|
"id": "totalEndpoints",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const config = data.flaskConfig || {}; const blueprints = config.blueprints || []; return blueprints.reduce((sum, bp) => sum + (bp.endpoints || []).length, 0); }",
|
"expression": "sumByLength(data.flaskConfig.blueprints, \"endpoints\")",
|
||||||
"dependencies": ["flaskConfig"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedLambda",
|
"id": "selectedLambda",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.lambdas?.find(l => l.id === data.selectedLambdaId) || null",
|
"expression": "findById(data.lambdas, data.selectedLambdaId)",
|
||||||
"dependencies": ["lambdas", "selectedLambdaId"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lambdaCount",
|
"id": "lambdaCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.lambdas || []).length",
|
"expression": "length(data.lambdas)",
|
||||||
"dependencies": ["lambdas"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -28,15 +28,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedModel",
|
"id": "selectedModel",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.models?.find(m => m.id === data.selectedModelId) || null",
|
"expression": "findById(data.models, data.selectedModelId)",
|
||||||
"dependencies": ["models", "selectedModelId"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "modelCount",
|
"id": "modelCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.models || []).length",
|
"expression": "length(data.models)",
|
||||||
"dependencies": ["models"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -17,15 +17,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "isRecent",
|
"id": "isRecent",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { if (!data.lastSaved) return false; return Date.now() - data.lastSaved < 3000; }",
|
"expression": "isRecentTimestamp(data.lastSaved, data.currentTime, 3000)",
|
||||||
"dependencies": ["lastSaved", "currentTime"]
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "timeAgo",
|
"id": "timeAgo",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"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`; }",
|
"expression": "timeAgo(data.lastSaved, data.currentTime)",
|
||||||
"dependencies": ["lastSaved", "currentTime"]
|
"defaultValue": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "div",
|
"type": "div",
|
||||||
|
|||||||
@@ -54,21 +54,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "activeVariant",
|
"id": "activeVariant",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const theme = data.theme || {}; const variants = theme.variants || []; return variants.find(v => v.id === theme.activeVariantId) || variants[0] || null; }",
|
"expression": "findByIdOrFirst(data.theme.variants, data.theme.activeVariantId)",
|
||||||
"dependencies": ["theme"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "variantCount",
|
"id": "variantCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => ((data.theme || {}).variants || []).length",
|
"expression": "length(data.theme.variants)",
|
||||||
"dependencies": ["theme"]
|
"defaultValue": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "customColorCount",
|
"id": "customColorCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => { const variant = data.activeVariant; if (!variant || !variant.colors) return 0; return Object.keys(variant.colors.customColors || {}).length; }",
|
"expression": "keyCount(data.activeVariant.colors.customColors)",
|
||||||
"dependencies": ["activeVariant"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -32,15 +32,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "selectedWorkflow",
|
"id": "selectedWorkflow",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => data.workflows?.find(w => w.id === data.selectedWorkflowId) || null",
|
"expression": "findById(data.workflows, data.selectedWorkflowId)",
|
||||||
"dependencies": ["workflows", "selectedWorkflowId"]
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "workflowCount",
|
"id": "workflowCount",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "(data) => (data.workflows || []).length",
|
"expression": "length(data.workflows)",
|
||||||
"dependencies": ["workflows"]
|
"defaultValue": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Binding Designer",
|
"title": "Data Binding Designer",
|
||||||
"description": "Connect UI components to KV storage and computed values"
|
"description": "Connect UI components to KV storage and static values"
|
||||||
},
|
},
|
||||||
"bindingsCard": {
|
"bindingsCard": {
|
||||||
"title": "Component Bindings",
|
"title": "Component Bindings",
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
"title": "How It Works",
|
"title": "How It Works",
|
||||||
"steps": [
|
"steps": [
|
||||||
"Create data sources (KV store for persistence, static for constants)",
|
"Create data sources (KV store for persistence, static for constants)",
|
||||||
"Add computed sources to derive values from other sources",
|
|
||||||
"Bind component properties to data sources for reactive updates"
|
"Bind component properties to data sources for reactive updates"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -33,12 +32,6 @@
|
|||||||
"type": "kv",
|
"type": "kv",
|
||||||
"key": "app-counter",
|
"key": "app-counter",
|
||||||
"defaultValue": 0
|
"defaultValue": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "displayName",
|
|
||||||
"type": "computed",
|
|
||||||
"dependencies": ["userProfile"],
|
|
||||||
"computeId": "displayName"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
@@ -50,7 +43,8 @@
|
|||||||
},
|
},
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"children": {
|
"children": {
|
||||||
"source": "displayName"
|
"source": "userProfile",
|
||||||
|
"path": "name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Edit Data Source",
|
"title": "Edit Data Source",
|
||||||
"description": "Configure the data source settings and dependencies",
|
"description": "Configure the data source settings",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": {
|
"id": {
|
||||||
"label": "ID",
|
"label": "ID",
|
||||||
@@ -18,14 +18,6 @@
|
|||||||
"valueLabel": "Value (JSON)",
|
"valueLabel": "Value (JSON)",
|
||||||
"valuePlaceholder": "{\"key\": \"value\"}"
|
"valuePlaceholder": "{\"key\": \"value\"}"
|
||||||
},
|
},
|
||||||
"computed": {
|
|
||||||
"computeLabel": "Compute Function",
|
|
||||||
"computePlaceholder": "(data) => data.source1 + data.source2",
|
|
||||||
"computeHelp": "Function that computes the value from other data sources",
|
|
||||||
"dependenciesLabel": "Dependencies",
|
|
||||||
"availableSourcesLabel": "Available Sources",
|
|
||||||
"emptyDependencies": "No data sources available. Create KV or static sources first."
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save Changes"
|
"save": "Save Changes"
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Data Sources",
|
"title": "Data Sources",
|
||||||
"description": "Manage KV storage, computed values, and static data"
|
"description": "Manage KV storage and static data"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"add": "Add Data Source"
|
"add": "Add Data Source"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"kv": "KV Store",
|
"kv": "KV Store",
|
||||||
"computed": "Computed Value",
|
|
||||||
"static": "Static Data"
|
"static": "Static Data"
|
||||||
},
|
},
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
@@ -17,12 +16,9 @@
|
|||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"kv": "KV Store",
|
"kv": "KV Store",
|
||||||
"static": "Static Data",
|
"static": "Static Data"
|
||||||
"computed": "Computed Values"
|
|
||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"deleteBlockedTitle": "Cannot delete",
|
|
||||||
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
|
|
||||||
"deleted": "Data source deleted",
|
"deleted": "Data source deleted",
|
||||||
"updated": "Data source updated"
|
"updated": "Data source updated"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
|
||||||
export function useDataSourceEditor(
|
export function useDataSourceEditor(
|
||||||
dataSource: DataSource | null,
|
dataSource: DataSource | null,
|
||||||
allDataSources: DataSource[],
|
|
||||||
) {
|
) {
|
||||||
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
||||||
|
|
||||||
@@ -15,44 +14,8 @@ export function useDataSourceEditor(
|
|||||||
setEditingSource(prev => (prev ? { ...prev, [field]: value } : prev))
|
setEditingSource(prev => (prev ? { ...prev, [field]: value } : prev))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource(prev => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
if (deps.includes(depId)) return prev
|
|
||||||
return { ...prev, dependencies: [...deps, depId] }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource(prev => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
return { ...prev, dependencies: deps.filter(dep => dep !== depId) }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const availableDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return allDataSources.filter(
|
|
||||||
ds => ds.id !== editingSource.id && ds.type !== 'computed',
|
|
||||||
)
|
|
||||||
}, [allDataSources, editingSource])
|
|
||||||
|
|
||||||
const selectedDeps = useMemo(() => editingSource?.dependencies || [], [editingSource])
|
|
||||||
|
|
||||||
const unselectedDeps = useMemo(
|
|
||||||
() => availableDeps.filter(ds => !selectedDeps.includes(ds.id)),
|
|
||||||
[availableDeps, selectedDeps],
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
|
||||||
removeDependency,
|
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
|||||||
id: `ds-${Date.now()}`,
|
id: `ds-${Date.now()}`,
|
||||||
type,
|
type,
|
||||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||||
...(type === 'computed' && { compute: () => null, dependencies: [] }),
|
|
||||||
...(type === 'static' && { defaultValue: null }),
|
...(type === 'static' && { defaultValue: null }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,19 +30,11 @@ export function useDataSourceManager(initialSources: DataSource[] = []) {
|
|||||||
return dataSources.find(ds => ds.id === id)
|
return dataSources.find(ds => ds.id === id)
|
||||||
}, [dataSources])
|
}, [dataSources])
|
||||||
|
|
||||||
const getDependents = useCallback((sourceId: string) => {
|
|
||||||
return dataSources.filter(ds =>
|
|
||||||
ds.type === 'computed' &&
|
|
||||||
ds.dependencies?.includes(sourceId)
|
|
||||||
)
|
|
||||||
}, [dataSources])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataSources,
|
dataSources,
|
||||||
addDataSource,
|
addDataSource,
|
||||||
updateDataSource,
|
updateDataSource,
|
||||||
deleteDataSource,
|
deleteDataSource,
|
||||||
getDataSource,
|
getDataSource,
|
||||||
getDependents,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { useKV } from '@/hooks/use-kv'
|
import { useKV } from '@/hooks/use-kv'
|
||||||
|
|
||||||
export type DataSourceType = 'kv' | 'static' | 'computed'
|
export type DataSourceType = 'kv' | 'static'
|
||||||
|
|
||||||
export interface DataSourceConfig<T = any> {
|
export interface DataSourceConfig<T = any> {
|
||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
key?: string
|
key?: string
|
||||||
defaultValue?: T
|
defaultValue?: T
|
||||||
compute?: (allData: Record<string, any>) => T
|
|
||||||
dependencies?: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useKVDataSource<T = any>(key: string, defaultValue?: T) {
|
export function useKVDataSource<T = any>(key: string, defaultValue?: T) {
|
||||||
@@ -18,13 +16,6 @@ export function useStaticDataSource<T = any>(defaultValue: T) {
|
|||||||
return [defaultValue, () => {}, () => {}] as const
|
return [defaultValue, () => {}, () => {}] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useComputedDataSource<T = any>(
|
|
||||||
compute: (allData: Record<string, any>) => T,
|
|
||||||
dependencies: Record<string, any>
|
|
||||||
) {
|
|
||||||
return compute(dependencies)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMultipleDataSources(_sources: DataSourceConfig[]) {
|
export function useMultipleDataSources(_sources: DataSourceConfig[]) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||||
import { useKV } from '@/hooks/use-kv'
|
import { useKV } from '@/hooks/use-kv'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { setNestedValue } from '@/lib/json-ui/utils'
|
import { setNestedValue } from '@/lib/json-ui/utils'
|
||||||
|
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||||
|
|
||||||
export function useDataSources(dataSources: DataSource[]) {
|
export function useDataSources(dataSources: DataSource[]) {
|
||||||
const [data, setData] = useState<Record<string, any>>({})
|
const [data, setData] = useState<Record<string, any>>({})
|
||||||
@@ -27,7 +29,7 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
if (kvIndex !== -1 && kvStates[kvIndex]) {
|
if (kvIndex !== -1 && kvStates[kvIndex]) {
|
||||||
newData[source.id] = kvStates[kvIndex][0]
|
newData[source.id] = kvStates[kvIndex][0]
|
||||||
}
|
}
|
||||||
} else if (source.type === 'static') {
|
} else if (source.type === 'static' && !source.expression && !source.valueTemplate) {
|
||||||
newData[source.id] = source.defaultValue
|
newData[source.id] = source.defaultValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -39,20 +41,32 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
initializeData()
|
initializeData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
const derivedData = useMemo(() => {
|
||||||
const computedSources = dataSources.filter(ds => ds.type === 'computed')
|
const result: Record<string, any> = {}
|
||||||
|
const context = { ...data }
|
||||||
|
|
||||||
computedSources.forEach(source => {
|
dataSources.forEach((source) => {
|
||||||
if (source.compute) {
|
if (!source.expression && !source.valueTemplate) return
|
||||||
const deps = source.dependencies || []
|
|
||||||
const hasAllDeps = deps.every(dep => dep in data)
|
|
||||||
|
|
||||||
if (hasAllDeps) {
|
let computedValue: any
|
||||||
const computedValue = source.compute(data)
|
if (source.expression) {
|
||||||
setData(prev => ({ ...prev, [source.id]: computedValue }))
|
computedValue = evaluateBindingExpression(source.expression, context, {
|
||||||
}
|
fallback: undefined,
|
||||||
}
|
label: `data source (${source.id})`,
|
||||||
})
|
})
|
||||||
|
} else if (source.valueTemplate) {
|
||||||
|
computedValue = evaluateTemplate(source.valueTemplate, { data: context })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computedValue === undefined && source.defaultValue !== undefined) {
|
||||||
|
computedValue = source.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[source.id] = computedValue
|
||||||
|
context[source.id] = computedValue
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
}, [data, dataSources])
|
}, [data, dataSources])
|
||||||
|
|
||||||
const updateData = useCallback((sourceId: string, value: any) => {
|
const updateData = useCallback((sourceId: string, value: any) => {
|
||||||
@@ -101,8 +115,10 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
})
|
})
|
||||||
}, [dataSources, kvSources, kvStates])
|
}, [dataSources, kvSources, kvStates])
|
||||||
|
|
||||||
|
const mergedData = useMemo(() => ({ ...data, ...derivedData }), [data, derivedData])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data: mergedData,
|
||||||
updateData,
|
updateData,
|
||||||
updatePath,
|
updatePath,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||||
|
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
|
||||||
export function useDataSources(dataSources: DataSource[]) {
|
export function useDataSources(dataSources: DataSource[]) {
|
||||||
const [data, setData] = useState<Record<string, any>>({})
|
const [data, setData] = useState<Record<string, any>>({})
|
||||||
@@ -12,8 +14,8 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
[dataSources]
|
[dataSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
const computedSources = useMemo(
|
const derivedSources = useMemo(
|
||||||
() => dataSources.filter((ds) => ds.type === 'computed'),
|
() => dataSources.filter((ds) => ds.expression || ds.valueTemplate),
|
||||||
[dataSources]
|
[dataSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
} catch {
|
} catch {
|
||||||
initialData[ds.id] = ds.defaultValue
|
initialData[ds.id] = ds.defaultValue
|
||||||
}
|
}
|
||||||
} else if (ds.type === 'static') {
|
} else if (ds.type === 'static' && !ds.expression && !ds.valueTemplate) {
|
||||||
initialData[ds.id] = ds.defaultValue
|
initialData[ds.id] = ds.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,15 +54,29 @@ export function useDataSources(dataSources: DataSource[]) {
|
|||||||
|
|
||||||
const computedData = useMemo(() => {
|
const computedData = useMemo(() => {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
const context = { ...data }
|
||||||
|
|
||||||
computedSources.forEach((ds) => {
|
derivedSources.forEach((ds) => {
|
||||||
if (ds.compute && typeof ds.compute === 'function') {
|
let value: any
|
||||||
result[ds.id] = ds.compute(data)
|
if (ds.expression) {
|
||||||
|
value = evaluateBindingExpression(ds.expression, context, {
|
||||||
|
fallback: undefined,
|
||||||
|
label: `data source (${ds.id})`,
|
||||||
|
})
|
||||||
|
} else if (ds.valueTemplate) {
|
||||||
|
value = evaluateTemplate(ds.valueTemplate, { data: context })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value === undefined && ds.defaultValue !== undefined) {
|
||||||
|
value = ds.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[ds.id] = value
|
||||||
|
context[ds.id] = value
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}, [computedSources, data])
|
}, [derivedSources, data])
|
||||||
|
|
||||||
const allData = useMemo(
|
const allData = useMemo(
|
||||||
() => ({ ...data, ...computedData }),
|
() => ({ ...data, ...computedData }),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useWorkflows } from '../data/use-workflows'
|
|||||||
import { useLambdas } from '../data/use-lambdas'
|
import { useLambdas } from '../data/use-lambdas'
|
||||||
import { useActions } from './use-actions'
|
import { useActions } from './use-actions'
|
||||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||||
|
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||||
|
|
||||||
export function usePage(schema: PageSchema) {
|
export function usePage(schema: PageSchema) {
|
||||||
const files = useFiles()
|
const files = useFiles()
|
||||||
@@ -44,16 +45,25 @@ export function usePage(schema: PageSchema) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema.data) {
|
if (schema.data) {
|
||||||
const computed: Record<string, any> = {}
|
const computed: Record<string, any> = {}
|
||||||
|
const context = { ...dataContext }
|
||||||
|
|
||||||
schema.data.forEach(source => {
|
schema.data.forEach(source => {
|
||||||
if (source.type === 'computed' && source.compute) {
|
if (source.type === 'static') {
|
||||||
computed[source.id] = evaluateBindingExpression(source.compute, dataContext, {
|
if (source.expression) {
|
||||||
fallback: undefined,
|
computed[source.id] = evaluateBindingExpression(source.expression, context, {
|
||||||
label: `computed data (${source.id})`,
|
fallback: source.defaultValue,
|
||||||
|
label: `data source (${source.id})`,
|
||||||
})
|
})
|
||||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
} else if (source.valueTemplate) {
|
||||||
|
computed[source.id] = evaluateTemplate(source.valueTemplate, { data: context })
|
||||||
|
} else if (source.defaultValue !== undefined) {
|
||||||
computed[source.id] = source.defaultValue
|
computed[source.id] = source.defaultValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computed[source.id] !== undefined) {
|
||||||
|
context[source.id] = computed[source.id]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setComputedData(computed)
|
setComputedData(computed)
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
|
|
||||||
interface UseDataSourceEditorParams {
|
interface UseDataSourceEditorParams {
|
||||||
dataSource: DataSource | null
|
dataSource: DataSource | null
|
||||||
allDataSources: DataSource[]
|
|
||||||
onSave: (dataSource: DataSource) => void
|
onSave: (dataSource: DataSource) => void
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDataSourceEditor({
|
export function useDataSourceEditor({
|
||||||
dataSource,
|
dataSource,
|
||||||
allDataSources,
|
|
||||||
onSave,
|
onSave,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: UseDataSourceEditorParams) {
|
}: UseDataSourceEditorParams) {
|
||||||
@@ -27,51 +25,15 @@ export function useDataSourceEditor({
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource((prev) => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
if (deps.includes(depId)) return prev
|
|
||||||
return { ...prev, dependencies: [...deps, depId] }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeDependency = useCallback((depId: string) => {
|
|
||||||
setEditingSource((prev) => {
|
|
||||||
if (!prev || prev.type !== 'computed') return prev
|
|
||||||
const deps = prev.dependencies || []
|
|
||||||
return { ...prev, dependencies: deps.filter((id) => id !== depId) }
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
if (!editingSource) return
|
if (!editingSource) return
|
||||||
onSave(editingSource)
|
onSave(editingSource)
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
}, [editingSource, onOpenChange, onSave])
|
}, [editingSource, onOpenChange, onSave])
|
||||||
|
|
||||||
const availableDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return allDataSources.filter(
|
|
||||||
(ds) => ds.id !== editingSource.id && ds.type !== 'computed',
|
|
||||||
)
|
|
||||||
}, [allDataSources, editingSource])
|
|
||||||
|
|
||||||
const selectedDeps = useMemo(() => editingSource?.dependencies || [], [editingSource])
|
|
||||||
|
|
||||||
const unselectedDeps = useMemo(() => {
|
|
||||||
if (!editingSource) return []
|
|
||||||
return availableDeps.filter((ds) => !selectedDeps.includes(ds.id))
|
|
||||||
}, [availableDeps, editingSource, selectedDeps])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editingSource,
|
editingSource,
|
||||||
updateField,
|
updateField,
|
||||||
addDependency,
|
|
||||||
removeDependency,
|
|
||||||
handleSave,
|
handleSave,
|
||||||
availableDeps,
|
|
||||||
selectedDeps,
|
|
||||||
unselectedDeps,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,121 @@ interface EvaluationContext {
|
|||||||
event?: any
|
event?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpressionFunction = (args: any[], context: EvaluationContext) => any
|
||||||
|
|
||||||
|
const parseArguments = (argsString: string): string[] => {
|
||||||
|
if (!argsString.trim()) return []
|
||||||
|
const args: string[] = []
|
||||||
|
let current = ''
|
||||||
|
let depth = 0
|
||||||
|
let inSingleQuote = false
|
||||||
|
let inDoubleQuote = false
|
||||||
|
|
||||||
|
for (let i = 0; i < argsString.length; i++) {
|
||||||
|
const char = argsString[i]
|
||||||
|
const prevChar = argsString[i - 1]
|
||||||
|
|
||||||
|
if (char === "'" && !inDoubleQuote && prevChar !== '\\') {
|
||||||
|
inSingleQuote = !inSingleQuote
|
||||||
|
} else if (char === '"' && !inSingleQuote && prevChar !== '\\') {
|
||||||
|
inDoubleQuote = !inDoubleQuote
|
||||||
|
} else if (!inSingleQuote && !inDoubleQuote) {
|
||||||
|
if (char === '(') depth += 1
|
||||||
|
if (char === ')') depth = Math.max(0, depth - 1)
|
||||||
|
if (char === ',' && depth === 0) {
|
||||||
|
args.push(current.trim())
|
||||||
|
current = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current += char
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.trim()) {
|
||||||
|
args.push(current.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressionFunctions: Record<string, ExpressionFunction> = {
|
||||||
|
findById: ([list, id]) => {
|
||||||
|
if (!Array.isArray(list)) return null
|
||||||
|
return list.find((item) => item?.id === id) ?? null
|
||||||
|
},
|
||||||
|
findByIdOrFirst: ([list, id]) => {
|
||||||
|
if (!Array.isArray(list)) return null
|
||||||
|
return list.find((item) => item?.id === id) ?? list[0] ?? null
|
||||||
|
},
|
||||||
|
length: ([value]) => {
|
||||||
|
if (Array.isArray(value) || typeof value === 'string') {
|
||||||
|
return value.length
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.keys(value).length
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
keyCount: ([value]) => {
|
||||||
|
if (!value || typeof value !== 'object') return 0
|
||||||
|
return Object.keys(value).length
|
||||||
|
},
|
||||||
|
sumByLength: ([list, path], context) => {
|
||||||
|
if (!Array.isArray(list)) return 0
|
||||||
|
const resolvedPath = typeof path === 'string' ? path : evaluateExpression(String(path), context)
|
||||||
|
if (!resolvedPath || typeof resolvedPath !== 'string') return 0
|
||||||
|
return list.reduce((sum, item) => {
|
||||||
|
const value = getNestedValue(item, resolvedPath)
|
||||||
|
const length = Array.isArray(value) || typeof value === 'string' ? value.length : 0
|
||||||
|
return sum + length
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
isRecentTimestamp: ([lastSaved, nowValue, thresholdValue]) => {
|
||||||
|
if (!lastSaved) return false
|
||||||
|
const now = typeof nowValue === 'number' ? nowValue : Date.now()
|
||||||
|
const threshold = typeof thresholdValue === 'number' ? thresholdValue : 3000
|
||||||
|
return now - lastSaved < threshold
|
||||||
|
},
|
||||||
|
timeAgo: ([lastSaved, nowValue]) => {
|
||||||
|
if (!lastSaved) return ''
|
||||||
|
const now = typeof nowValue === 'number' ? nowValue : Date.now()
|
||||||
|
const seconds = Math.floor((now - 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`
|
||||||
|
},
|
||||||
|
filterUsersByQuery: ([users, query]) => {
|
||||||
|
const list = Array.isArray(users) ? users : []
|
||||||
|
const normalizedQuery = typeof query === 'string' ? query.toLowerCase() : ''
|
||||||
|
if (!normalizedQuery) return list
|
||||||
|
return list.filter((user) => {
|
||||||
|
const name = String(user?.name ?? '').toLowerCase()
|
||||||
|
const email = String(user?.email ?? '').toLowerCase()
|
||||||
|
return name.includes(normalizedQuery) || email.includes(normalizedQuery)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
userStats: ([users]) => {
|
||||||
|
const list = Array.isArray(users) ? users : []
|
||||||
|
return {
|
||||||
|
total: list.length,
|
||||||
|
active: list.filter((user) => user?.status === 'active').length,
|
||||||
|
inactive: list.filter((user) => user?.status === 'inactive').length,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
todoStats: ([todos]) => {
|
||||||
|
const list = Array.isArray(todos) ? todos : []
|
||||||
|
return {
|
||||||
|
total: list.length,
|
||||||
|
completed: list.filter((todo) => Boolean(todo?.completed)).length,
|
||||||
|
remaining: list.filter((todo) => !todo?.completed).length,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportedExpressionFunctions = new Set(Object.keys(expressionFunctions))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely evaluate a JSON expression
|
* Safely evaluate a JSON expression
|
||||||
* Supports:
|
* Supports:
|
||||||
@@ -26,6 +141,17 @@ export function evaluateExpression(
|
|||||||
const { data, event } = context
|
const { data, event } = context
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const functionMatch = expression.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\((.*)\)$/)
|
||||||
|
if (functionMatch) {
|
||||||
|
const functionName = functionMatch[1]
|
||||||
|
const argString = functionMatch[2]
|
||||||
|
const handler = expressionFunctions[functionName]
|
||||||
|
if (handler) {
|
||||||
|
const args = parseArguments(argString).map((arg) => evaluateExpression(arg, context))
|
||||||
|
return handler(args, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (expression === 'event') {
|
if (expression === 'event') {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { evaluateCondition, evaluateExpression } from './expression-evaluator'
|
import { evaluateCondition, evaluateExpression, supportedExpressionFunctions } from './expression-evaluator'
|
||||||
|
|
||||||
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
||||||
const NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/
|
const NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/
|
||||||
@@ -10,6 +10,8 @@ interface EvaluationOptions {
|
|||||||
event?: any
|
event?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FUNCTION_PATTERN = /^([A-Za-z_$][A-Za-z0-9_$]*)\((.*)\)$/
|
||||||
|
|
||||||
const isSupportedExpression = (expression: string) => {
|
const isSupportedExpression = (expression: string) => {
|
||||||
if (expression === 'event' || expression === 'data') return true
|
if (expression === 'event' || expression === 'data') return true
|
||||||
if (expression.startsWith('data.') || expression.startsWith('event.')) return true
|
if (expression.startsWith('data.') || expression.startsWith('event.')) return true
|
||||||
@@ -17,6 +19,10 @@ const isSupportedExpression = (expression: string) => {
|
|||||||
if (STRING_PATTERN.test(expression)) return true
|
if (STRING_PATTERN.test(expression)) return true
|
||||||
if (NUMBER_PATTERN.test(expression)) return true
|
if (NUMBER_PATTERN.test(expression)) return true
|
||||||
if (['true', 'false', 'null', 'undefined'].includes(expression)) return true
|
if (['true', 'false', 'null', 'undefined'].includes(expression)) return true
|
||||||
|
const functionMatch = expression.match(FUNCTION_PATTERN)
|
||||||
|
if (functionMatch) {
|
||||||
|
return supportedExpressionFunctions.has(functionMatch[1])
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export function useJSONDataSource<T = unknown>(
|
|||||||
) {
|
) {
|
||||||
const kvConfig = config.type === 'kv' ? config.config : undefined
|
const kvConfig = config.type === 'kv' ? config.config : undefined
|
||||||
const apiConfig = config.type === 'api' ? config.config : undefined
|
const apiConfig = config.type === 'api' ? config.config : undefined
|
||||||
const computedConfig = config.type === 'computed' ? config.config : undefined
|
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
config.type === 'static' ? config.config : config.config?.defaultValue
|
config.type === 'static' ? config.config : config.config?.defaultValue
|
||||||
|
|
||||||
@@ -57,8 +56,6 @@ export function useJSONDataSource<T = unknown>(
|
|||||||
return apiValue
|
return apiValue
|
||||||
case 'static':
|
case 'static':
|
||||||
return config.config
|
return config.config
|
||||||
case 'computed':
|
|
||||||
return computedConfig?.defaultValue
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export const PageUISchema = z.object({
|
|||||||
tables: z.array(TableSchema).optional(),
|
tables: z.array(TableSchema).optional(),
|
||||||
menus: z.array(MenuSchema).optional(),
|
menus: z.array(MenuSchema).optional(),
|
||||||
dataSources: z.record(z.string(), z.object({
|
dataSources: z.record(z.string(), z.object({
|
||||||
type: z.enum(['kv', 'api', 'computed', 'static']),
|
type: z.enum(['kv', 'api', 'static']),
|
||||||
config: z.any(),
|
config: z.any(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
})
|
})
|
||||||
@@ -242,13 +242,6 @@ export type DataSourceConfig<T = unknown> =
|
|||||||
transform?: (data: unknown) => T
|
transform?: (data: unknown) => T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'computed'
|
|
||||||
config: {
|
|
||||||
defaultValue?: T
|
|
||||||
transform?: (data: unknown) => T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'static'
|
type: 'static'
|
||||||
config: T
|
config: T
|
||||||
|
|||||||
@@ -22,15 +22,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filteredUsers",
|
"id": "filteredUsers",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "computeFilteredUsers",
|
"expression": "filterUsersByQuery(data.users, data.filterQuery)",
|
||||||
"dependencies": ["users", "filterQuery"]
|
"defaultValue": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "computeStats",
|
"expression": "userStats(data.users)",
|
||||||
"dependencies": ["users"]
|
"defaultValue": {
|
||||||
|
"total": 0,
|
||||||
|
"active": 0,
|
||||||
|
"inactive": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -16,20 +16,7 @@ export function hydrateSchema(jsonSchema: any): PageSchema {
|
|||||||
const schema = { ...jsonSchema }
|
const schema = { ...jsonSchema }
|
||||||
|
|
||||||
if (schema.dataSources) {
|
if (schema.dataSources) {
|
||||||
schema.dataSources = schema.dataSources.map((ds: any) => {
|
schema.dataSources = schema.dataSources.map((ds: any) => ds)
|
||||||
if (ds.type === 'computed' && typeof ds.compute === 'string') {
|
|
||||||
const functionName = ds.compute as keyof ComputeFunctionMap
|
|
||||||
const computeFunction = computeFunctions[functionName]
|
|
||||||
if (!computeFunction) {
|
|
||||||
console.warn(`Compute function "${functionName}" not found`)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...ds,
|
|
||||||
compute: computeFunction || (() => null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ds
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.components) {
|
if (schema.components) {
|
||||||
|
|||||||
@@ -22,9 +22,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "stats",
|
"id": "stats",
|
||||||
"type": "computed",
|
"type": "static",
|
||||||
"compute": "computeTodoStats",
|
"expression": "todoStats(data.todos)",
|
||||||
"dependencies": ["todos"]
|
"defaultValue": {
|
||||||
|
"total": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"remaining": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export type ActionType =
|
|||||||
| 'custom'
|
| 'custom'
|
||||||
|
|
||||||
export type DataSourceType =
|
export type DataSourceType =
|
||||||
| 'kv' | 'computed' | 'static'
|
| 'kv' | 'static'
|
||||||
|
|
||||||
export type BindingSourceType =
|
export type BindingSourceType =
|
||||||
| 'data' | 'bindings' | 'state'
|
| 'data' | 'bindings' | 'state'
|
||||||
@@ -52,8 +52,8 @@ export interface DataSource {
|
|||||||
type: DataSourceType
|
type: DataSourceType
|
||||||
key?: string
|
key?: string
|
||||||
defaultValue?: any
|
defaultValue?: any
|
||||||
compute?: (data: Record<string, any>) => any
|
expression?: string
|
||||||
dependencies?: string[]
|
valueTemplate?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ export const ComponentSchema: z.ZodType<any> = z.lazy(() =>
|
|||||||
|
|
||||||
export const DataSourceSchema = z.object({
|
export const DataSourceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.enum(['kv', 'computed', 'static', 'ai'], { message: 'Invalid data source type' }),
|
type: z.enum(['kv', 'static', 'ai'], { message: 'Invalid data source type' }),
|
||||||
key: z.string().optional(),
|
key: z.string().optional(),
|
||||||
defaultValue: z.any().optional(),
|
defaultValue: z.any().optional(),
|
||||||
dependencies: z.array(z.string()).optional(),
|
expression: z.string().optional(),
|
||||||
compute: z.string().optional(),
|
valueTemplate: z.record(z.string(), z.any()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ActionConfigSchema = z.object({
|
export const ActionConfigSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user