mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Refactor data binding designer copy
This commit is contained in:
@@ -1,61 +1,39 @@
|
||||
import { useState } from 'react'
|
||||
import { DataSourceManager } from '@/components/organisms/DataSourceManager'
|
||||
import { ComponentBindingDialog } from '@/components/molecules/ComponentBindingDialog'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DataSource, UIComponent } from '@/types/json-ui'
|
||||
import { Link, Code } from '@phosphor-icons/react'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { DataBindingHeader } from '@/components/data-binding-designer/DataBindingHeader'
|
||||
import { ComponentBindingsCard } from '@/components/data-binding-designer/ComponentBindingsCard'
|
||||
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
|
||||
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() {
|
||||
const [dataSources, setDataSources] = useState<DataSource[]>([
|
||||
{
|
||||
id: 'userProfile',
|
||||
type: 'kv',
|
||||
key: 'user-profile',
|
||||
defaultValue: { name: 'John Doe', email: 'john@example.com' },
|
||||
},
|
||||
{
|
||||
id: 'counter',
|
||||
type: 'kv',
|
||||
key: 'app-counter',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
type: 'computed',
|
||||
compute: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
|
||||
dependencies: ['userProfile'],
|
||||
},
|
||||
])
|
||||
const [dataSources, setDataSources] = useState<DataSource[]>(
|
||||
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
|
||||
)
|
||||
|
||||
const [mockComponents] = useState<UIComponent[]>([
|
||||
{
|
||||
id: 'title',
|
||||
type: 'Heading',
|
||||
props: { className: 'text-2xl font-bold' },
|
||||
bindings: {
|
||||
children: { source: 'displayName' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'counter-display',
|
||||
type: 'Text',
|
||||
props: { className: 'text-lg' },
|
||||
bindings: {
|
||||
children: { source: 'counter' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'email-input',
|
||||
type: 'Input',
|
||||
props: { placeholder: 'Enter email' },
|
||||
bindings: {
|
||||
value: { source: 'userProfile', path: 'email' },
|
||||
},
|
||||
},
|
||||
])
|
||||
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)
|
||||
|
||||
const [selectedComponent, setSelectedComponent] = useState<UIComponent | null>(null)
|
||||
const [bindingDialogOpen, setBindingDialogOpen] = useState(false)
|
||||
@@ -69,21 +47,13 @@ export function DataBindingDesigner() {
|
||||
console.log('Updated component bindings:', updatedComponent)
|
||||
}
|
||||
|
||||
const getSourceById = (sourceId: string) => {
|
||||
return dataSources.find(ds => ds.id === sourceId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||
Data Binding Designer
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Connect UI components to KV storage and computed values
|
||||
</p>
|
||||
</div>
|
||||
<DataBindingHeader
|
||||
title={dataBindingCopy.header.title}
|
||||
description={dataBindingCopy.header.description}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-6">
|
||||
@@ -94,121 +64,17 @@ export function DataBindingDesigner() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code className="w-5 h-5" />
|
||||
Component Bindings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Example components with data bindings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[600px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{mockComponents.map(component => {
|
||||
const bindingCount = Object.keys(component.bindings || {}).length
|
||||
|
||||
return (
|
||||
<Card key={component.id} className="bg-card/50 backdrop-blur">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{component.type}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
#{component.id}
|
||||
</span>
|
||||
</div>
|
||||
<ComponentBindingsCard
|
||||
components={mockComponents}
|
||||
dataSources={dataSources}
|
||||
copy={dataBindingCopy.bindingsCard}
|
||||
onEditBinding={handleEditBinding}
|
||||
/>
|
||||
|
||||
{bindingCount > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(component.bindings || {}).map(([prop, binding]) => {
|
||||
const source = getSourceById(binding.source)
|
||||
return (
|
||||
<div key={prop} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{prop}:
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="font-mono h-5 text-xs"
|
||||
>
|
||||
{binding.source}
|
||||
{binding.path && `.${binding.path}`}
|
||||
</Badge>
|
||||
{source && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-5 text-xs"
|
||||
>
|
||||
{source.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No bindings configured
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleEditBinding(component)}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Link className="w-4 h-4 mr-1" />
|
||||
Bind
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-accent/5 border-accent/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">How It Works</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||
1
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Create data sources (KV store for persistence, static for constants)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||
2
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Add computed sources to derive values from other sources
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||
3
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Bind component properties to data sources for reactive updates
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<HowItWorksCard
|
||||
title={dataBindingCopy.howItWorks.title}
|
||||
steps={dataBindingCopy.howItWorks.steps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
115
src/components/data-binding-designer/ComponentBindingsCard.tsx
Normal file
115
src/components/data-binding-designer/ComponentBindingsCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { DataSource, UIComponent } from '@/types/json-ui'
|
||||
import { Link, Code } from '@phosphor-icons/react'
|
||||
|
||||
interface ComponentBindingsCardCopy {
|
||||
title: string
|
||||
description: string
|
||||
emptyState: string
|
||||
actionLabel: string
|
||||
}
|
||||
|
||||
interface ComponentBindingsCardProps {
|
||||
components: UIComponent[]
|
||||
dataSources: DataSource[]
|
||||
copy: ComponentBindingsCardCopy
|
||||
onEditBinding: (component: UIComponent) => void
|
||||
}
|
||||
|
||||
export function ComponentBindingsCard({
|
||||
components,
|
||||
dataSources,
|
||||
copy,
|
||||
onEditBinding,
|
||||
}: ComponentBindingsCardProps) {
|
||||
const getSourceById = (sourceId: string) => dataSources.find(ds => ds.id === sourceId)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code className="w-5 h-5" />
|
||||
{copy.title}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{copy.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[600px] pr-4">
|
||||
<div className="space-y-3">
|
||||
{components.map(component => {
|
||||
const bindingCount = Object.keys(component.bindings || {}).length
|
||||
|
||||
return (
|
||||
<Card key={component.id} className="bg-card/50 backdrop-blur">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{component.type}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
#{component.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{bindingCount > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(component.bindings || {}).map(([prop, binding]) => {
|
||||
const source = getSourceById(binding.source)
|
||||
return (
|
||||
<div key={prop} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{prop}:
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="font-mono h-5 text-xs"
|
||||
>
|
||||
{binding.source}
|
||||
{binding.path && `.${binding.path}`}
|
||||
</Badge>
|
||||
{source && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-5 text-xs"
|
||||
>
|
||||
{source.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.emptyState}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onEditBinding(component)}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Link className="w-4 h-4 mr-1" />
|
||||
{copy.actionLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
17
src/components/data-binding-designer/DataBindingHeader.tsx
Normal file
17
src/components/data-binding-designer/DataBindingHeader.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
interface DataBindingHeaderProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export function DataBindingHeader({ title, description }: DataBindingHeaderProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
src/components/data-binding-designer/HowItWorksCard.tsx
Normal file
28
src/components/data-binding-designer/HowItWorksCard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
interface HowItWorksCardProps {
|
||||
title: string
|
||||
steps: string[]
|
||||
}
|
||||
|
||||
export function HowItWorksCard({ title, steps }: HowItWorksCardProps) {
|
||||
return (
|
||||
<Card className="bg-accent/5 border-accent/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step} className="flex gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary/20 text-primary flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{step}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
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'
|
||||
|
||||
interface DataSourceEditorDialogProps {
|
||||
open: boolean
|
||||
@@ -18,15 +17,19 @@ interface DataSourceEditorDialogProps {
|
||||
onSave: (dataSource: DataSource) => void
|
||||
}
|
||||
|
||||
export function DataSourceEditorDialog({
|
||||
open,
|
||||
dataSource,
|
||||
export function DataSourceEditorDialog({
|
||||
open,
|
||||
dataSource,
|
||||
allDataSources,
|
||||
onOpenChange,
|
||||
onSave
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: DataSourceEditorDialogProps) {
|
||||
const [editingSource, setEditingSource] = useState<DataSource | null>(dataSource)
|
||||
|
||||
useEffect(() => {
|
||||
setEditingSource(dataSource)
|
||||
}, [dataSource])
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editingSource) return
|
||||
onSave(editingSource)
|
||||
@@ -55,7 +58,7 @@ export function DataSourceEditorDialog({
|
||||
if (!editingSource) return null
|
||||
|
||||
const availableDeps = allDataSources.filter(
|
||||
ds => ds.id !== editingSource.id && ds.type !== 'computed'
|
||||
ds => ds.id !== editingSource.id && ds.type !== 'computed',
|
||||
)
|
||||
|
||||
const selectedDeps = editingSource.dependencies || []
|
||||
@@ -66,158 +69,59 @@ export function DataSourceEditorDialog({
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Edit Data Source
|
||||
{dataSourceEditorCopy.title}
|
||||
<DataSourceBadge type={editingSource.type} />
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure the data source settings and dependencies
|
||||
{dataSourceEditorCopy.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>ID</Label>
|
||||
<Input
|
||||
value={editingSource.id}
|
||||
onChange={(e) => updateField('id', e.target.value)}
|
||||
placeholder="unique-id"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<DataSourceIdField
|
||||
editingSource={editingSource}
|
||||
label={dataSourceEditorCopy.fields.id.label}
|
||||
placeholder={dataSourceEditorCopy.fields.id.placeholder}
|
||||
onChange={(value) => updateField('id', value)}
|
||||
/>
|
||||
|
||||
{editingSource.type === 'kv' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>KV Store Key</Label>
|
||||
<Input
|
||||
value={editingSource.key || ''}
|
||||
onChange={(e) => updateField('key', e.target.value)}
|
||||
placeholder="storage-key"
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Key used for persistent storage in the KV store
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Default Value (JSON)</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(editingSource.defaultValue, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value)
|
||||
updateField('defaultValue', parsed)
|
||||
} catch (err) {
|
||||
// Invalid JSON, don't update
|
||||
}
|
||||
}}
|
||||
placeholder='{"key": "value"}'
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<KvSourceFields
|
||||
editingSource={editingSource}
|
||||
copy={dataSourceEditorCopy.kv}
|
||||
onUpdateField={updateField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingSource.type === 'static' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Value (JSON)</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(editingSource.defaultValue, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value)
|
||||
updateField('defaultValue', parsed)
|
||||
} catch (err) {
|
||||
// Invalid JSON, don't update
|
||||
}
|
||||
}}
|
||||
placeholder='{"key": "value"}'
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
<StaticSourceFields
|
||||
editingSource={editingSource}
|
||||
label={dataSourceEditorCopy.static.valueLabel}
|
||||
placeholder={dataSourceEditorCopy.static.valuePlaceholder}
|
||||
onUpdateField={updateField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingSource.type === 'computed' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Compute Function</Label>
|
||||
<Textarea
|
||||
value={editingSource.compute?.toString() || ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const fn = new Function('data', `return (${e.target.value})`)()
|
||||
updateField('compute', fn)
|
||||
} catch (err) {
|
||||
// Invalid function
|
||||
}
|
||||
}}
|
||||
placeholder="(data) => data.source1 + data.source2"
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Function that computes the value from other data sources
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Dependencies</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={() => removeDependency(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">Available Sources</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{unselectedDeps.map(ds => (
|
||||
<Button
|
||||
key={ds.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => addDependency(ds.id)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
+ {ds.id}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unselectedDeps.length === 0 && selectedDeps.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No data sources available. Create KV or static sources first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<ComputedSourceFields
|
||||
editingSource={editingSource}
|
||||
availableDeps={availableDeps}
|
||||
selectedDeps={selectedDeps}
|
||||
unselectedDeps={unselectedDeps}
|
||||
copy={dataSourceEditorCopy.computed}
|
||||
onUpdateField={updateField}
|
||||
onAddDependency={addDependency}
|
||||
onRemoveDependency={removeDependency}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{dataSourceEditorCopy.actions.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Save Changes
|
||||
{dataSourceEditorCopy.actions.save}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
|
||||
interface DataSourceIdFieldProps {
|
||||
editingSource: DataSource
|
||||
label: string
|
||||
placeholder: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function DataSourceIdField({
|
||||
editingSource,
|
||||
label,
|
||||
placeholder,
|
||||
onChange,
|
||||
}: DataSourceIdFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>{label}</Label>
|
||||
<Input
|
||||
value={editingSource.id}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
|
||||
interface KvSourceFieldsCopy {
|
||||
keyLabel: string
|
||||
keyPlaceholder: string
|
||||
keyHelp: string
|
||||
defaultLabel: string
|
||||
defaultPlaceholder: string
|
||||
}
|
||||
|
||||
interface KvSourceFieldsProps {
|
||||
editingSource: DataSource
|
||||
copy: KvSourceFieldsCopy
|
||||
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
|
||||
}
|
||||
|
||||
export function KvSourceFields({ editingSource, copy, onUpdateField }: KvSourceFieldsProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.keyLabel}</Label>
|
||||
<Input
|
||||
value={editingSource.key || ''}
|
||||
onChange={(e) => onUpdateField('key', e.target.value)}
|
||||
placeholder={copy.keyPlaceholder}
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.keyHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.defaultLabel}</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(editingSource.defaultValue, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value)
|
||||
onUpdateField('defaultValue', parsed)
|
||||
} catch (err) {
|
||||
// Invalid JSON, don't update
|
||||
}
|
||||
}}
|
||||
placeholder={copy.defaultPlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
|
||||
interface StaticSourceFieldsProps {
|
||||
editingSource: DataSource
|
||||
label: string
|
||||
placeholder: string
|
||||
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
|
||||
}
|
||||
|
||||
export function StaticSourceFields({
|
||||
editingSource,
|
||||
label,
|
||||
placeholder,
|
||||
onUpdateField,
|
||||
}: StaticSourceFieldsProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>{label}</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(editingSource.defaultValue, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value)
|
||||
onUpdateField('defaultValue', parsed)
|
||||
} catch (err) {
|
||||
// Invalid JSON, don't update
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { DataSourceCard } from '@/components/molecules/DataSourceCard'
|
||||
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 { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
EmptyState,
|
||||
ActionButton,
|
||||
Heading,
|
||||
Text,
|
||||
IconText,
|
||||
Stack,
|
||||
Section
|
||||
} from '@/components/atoms'
|
||||
import { EmptyState, Stack } from '@/components/atoms'
|
||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||
import { DataSourceGroupSection } from '@/components/organisms/data-source-manager/DataSourceGroupSection'
|
||||
import dataSourceManagerCopy from '@/data/data-source-manager.json'
|
||||
|
||||
interface DataSourceManagerProps {
|
||||
dataSources: DataSource[]
|
||||
@@ -56,21 +44,24 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
const handleDeleteSource = (id: string) => {
|
||||
const dependents = getDependents(id)
|
||||
if (dependents.length > 0) {
|
||||
toast.error('Cannot delete', {
|
||||
description: `This source is used by ${dependents.length} computed ${dependents.length === 1 ? 'source' : 'sources'}`,
|
||||
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)
|
||||
onChange(localSources.filter(ds => ds.id !== id))
|
||||
toast.success('Data source deleted')
|
||||
toast.success(dataSourceManagerCopy.toasts.deleted)
|
||||
}
|
||||
|
||||
const handleSaveSource = (updatedSource: DataSource) => {
|
||||
updateDataSource(updatedSource.id, updatedSource)
|
||||
onChange(localSources.map(ds => ds.id === updatedSource.id ? updatedSource : ds))
|
||||
toast.success('Data source updated')
|
||||
toast.success(dataSourceManagerCopy.toasts.updated)
|
||||
}
|
||||
|
||||
const groupedSources = {
|
||||
@@ -83,115 +74,51 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<Stack direction="vertical" spacing="xs">
|
||||
<Heading level={2}>Data Sources</Heading>
|
||||
<Text variant="muted">
|
||||
Manage KV storage, computed values, and static data
|
||||
</Text>
|
||||
</Stack>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<ActionButton
|
||||
icon={<Plus size={16} />}
|
||||
label="Add Data Source"
|
||||
variant="default"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleAddDataSource('kv')}>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
KV Store
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleAddDataSource('computed')}>
|
||||
<Function className="w-4 h-4 mr-2" />
|
||||
Computed Value
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleAddDataSource('static')}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Static Data
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DataSourceManagerHeader
|
||||
copy={{
|
||||
title: dataSourceManagerCopy.header.title,
|
||||
description: dataSourceManagerCopy.header.description,
|
||||
addLabel: dataSourceManagerCopy.actions.add,
|
||||
menu: dataSourceManagerCopy.menu,
|
||||
}}
|
||||
onAdd={handleAddDataSource}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{localSources.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Database size={48} weight="duotone" />}
|
||||
title="No data sources yet"
|
||||
description="Create your first data source to start binding data to components"
|
||||
title={dataSourceManagerCopy.emptyState.title}
|
||||
description={dataSourceManagerCopy.emptyState.description}
|
||||
/>
|
||||
) : (
|
||||
<Stack direction="vertical" spacing="xl">
|
||||
{groupedSources.kv.length > 0 && (
|
||||
<Section>
|
||||
<IconText
|
||||
icon={<Database size={16} />}
|
||||
className="text-sm font-semibold mb-3"
|
||||
>
|
||||
KV Store ({groupedSources.kv.length})
|
||||
</IconText>
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{groupedSources.kv.map(ds => (
|
||||
<DataSourceCard
|
||||
key={ds.id}
|
||||
dataSource={ds}
|
||||
dependents={getDependents(ds.id)}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
)}
|
||||
<DataSourceGroupSection
|
||||
icon={<Database size={16} />}
|
||||
label={dataSourceManagerCopy.groups.kv}
|
||||
dataSources={groupedSources.kv}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
|
||||
{groupedSources.static.length > 0 && (
|
||||
<Section>
|
||||
<IconText
|
||||
icon={<FileText size={16} />}
|
||||
className="text-sm font-semibold mb-3"
|
||||
>
|
||||
Static Data ({groupedSources.static.length})
|
||||
</IconText>
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{groupedSources.static.map(ds => (
|
||||
<DataSourceCard
|
||||
key={ds.id}
|
||||
dataSource={ds}
|
||||
dependents={getDependents(ds.id)}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
)}
|
||||
<DataSourceGroupSection
|
||||
icon={<FileText size={16} />}
|
||||
label={dataSourceManagerCopy.groups.static}
|
||||
dataSources={groupedSources.static}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
|
||||
{groupedSources.computed.length > 0 && (
|
||||
<Section>
|
||||
<IconText
|
||||
icon={<Function size={16} />}
|
||||
className="text-sm font-semibold mb-3"
|
||||
>
|
||||
Computed Values ({groupedSources.computed.length})
|
||||
</IconText>
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{groupedSources.computed.map(ds => (
|
||||
<DataSourceCard
|
||||
key={ds.id}
|
||||
dataSource={ds}
|
||||
dependents={getDependents(ds.id)}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
)}
|
||||
<DataSourceGroupSection
|
||||
icon={<Function size={16} />}
|
||||
label={dataSourceManagerCopy.groups.computed}
|
||||
dataSources={groupedSources.computed}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { DataSourceCard } from '@/components/molecules/DataSourceCard'
|
||||
import { IconText, Section, Stack } from '@/components/atoms'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface DataSourceGroupSectionProps {
|
||||
icon: ReactNode
|
||||
label: string
|
||||
dataSources: DataSource[]
|
||||
getDependents: (id: string) => string[]
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function DataSourceGroupSection({
|
||||
icon,
|
||||
label,
|
||||
dataSources,
|
||||
getDependents,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: DataSourceGroupSectionProps) {
|
||||
if (dataSources.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<IconText
|
||||
icon={icon}
|
||||
className="text-sm font-semibold mb-3"
|
||||
>
|
||||
{label} ({dataSources.length})
|
||||
</IconText>
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{dataSources.map(ds => (
|
||||
<DataSourceCard
|
||||
key={ds.id}
|
||||
dataSource={ds}
|
||||
dependents={getDependents(ds.id)}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
|
||||
interface DataSourceManagerHeaderCopy {
|
||||
title: string
|
||||
description: string
|
||||
addLabel: string
|
||||
menu: {
|
||||
kv: string
|
||||
computed: string
|
||||
static: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DataSourceManagerHeaderProps {
|
||||
copy: DataSourceManagerHeaderCopy
|
||||
onAdd: (type: DataSourceType) => void
|
||||
}
|
||||
|
||||
export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Stack direction="vertical" spacing="xs">
|
||||
<Heading level={2}>{copy.title}</Heading>
|
||||
<Text variant="muted">
|
||||
{copy.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<ActionButton
|
||||
icon={<Plus size={16} />}
|
||||
label={copy.addLabel}
|
||||
variant="default"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onAdd('kv')}>
|
||||
<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}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
84
src/data/data-binding-designer.json
Normal file
84
src/data/data-binding-designer.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Data Binding Designer",
|
||||
"description": "Connect UI components to KV storage and computed values"
|
||||
},
|
||||
"bindingsCard": {
|
||||
"title": "Component Bindings",
|
||||
"description": "Example components with data bindings",
|
||||
"emptyState": "No bindings configured",
|
||||
"actionLabel": "Bind"
|
||||
},
|
||||
"howItWorks": {
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"seed": {
|
||||
"dataSources": [
|
||||
{
|
||||
"id": "userProfile",
|
||||
"type": "kv",
|
||||
"key": "user-profile",
|
||||
"defaultValue": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "counter",
|
||||
"type": "kv",
|
||||
"key": "app-counter",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"type": "computed",
|
||||
"dependencies": ["userProfile"],
|
||||
"computeId": "displayName"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "title",
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"className": "text-2xl font-bold"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "displayName"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "counter-display",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-lg"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "counter"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "email-input",
|
||||
"type": "Input",
|
||||
"props": {
|
||||
"placeholder": "Enter email"
|
||||
},
|
||||
"bindings": {
|
||||
"value": {
|
||||
"source": "userProfile",
|
||||
"path": "email"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
33
src/data/data-source-editor-dialog.json
Normal file
33
src/data/data-source-editor-dialog.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"title": "Edit Data Source",
|
||||
"description": "Configure the data source settings and dependencies",
|
||||
"fields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"placeholder": "unique-id"
|
||||
}
|
||||
},
|
||||
"kv": {
|
||||
"keyLabel": "KV Store Key",
|
||||
"keyPlaceholder": "storage-key",
|
||||
"keyHelp": "Key used for persistent storage in the KV store",
|
||||
"defaultLabel": "Default Value (JSON)",
|
||||
"defaultPlaceholder": "{\"key\": \"value\"}"
|
||||
},
|
||||
"static": {
|
||||
"valueLabel": "Value (JSON)",
|
||||
"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": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Changes"
|
||||
}
|
||||
}
|
||||
29
src/data/data-source-manager.json
Normal file
29
src/data/data-source-manager.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Data Sources",
|
||||
"description": "Manage KV storage, computed values, and static data"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add Data Source"
|
||||
},
|
||||
"menu": {
|
||||
"kv": "KV Store",
|
||||
"computed": "Computed Value",
|
||||
"static": "Static Data"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No data sources yet",
|
||||
"description": "Create your first data source to start binding data to components"
|
||||
},
|
||||
"groups": {
|
||||
"kv": "KV Store",
|
||||
"static": "Static Data",
|
||||
"computed": "Computed Values"
|
||||
},
|
||||
"toasts": {
|
||||
"deleteBlockedTitle": "Cannot delete",
|
||||
"deleteBlockedDescription": "This source is used by {count} computed {noun}",
|
||||
"deleted": "Data source deleted",
|
||||
"updated": "Data source updated"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user