feat: Convert ComponentTree and SeedDataManager to pure JSON with hooks

- Created useComponentTree hook to flatten recursive tree structure
- Created useStorageBackendInfo hook for backend icon/copy logic
- Created useD3BarChart hook for chart calculations
- Registered all new hooks in hooks-registry
- Created component-tree.json with list rendering using treeData from hook
- Created seed-data-manager.json with full Card/Alert/Button structure
- Deleted ComponentTreeWrapper.tsx and SeedDataManagerWrapper.tsx
- Updated exports to use JSON components
- 9 components now pure JSON, 2 wrappers remaining

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-18 23:48:23 +00:00
parent db8759f9e0
commit e549f4e0de
10 changed files with 455 additions and 187 deletions

View File

@@ -0,0 +1,65 @@
{
"id": "component-tree-container",
"type": "div",
"bindings": {
"className": {
"source": "className",
"transform": "data ? `space-y-2 ${data}` : 'space-y-2'"
}
},
"children": [
{
"id": "empty-message",
"type": "p",
"props": { "className": "text-sm text-muted-foreground" },
"bindings": { "children": "emptyMessage" },
"conditional": { "if": "components.length === 0" }
},
{
"id": "tree-nodes",
"type": "div",
"props": { "className": "space-y-1" },
"conditional": { "if": "components.length > 0" },
"children": [
{
"id": "tree-node-list",
"type": "list",
"bindings": {
"items": "treeData",
"keyPath": "component.id"
},
"itemTemplate": {
"type": "button",
"props": { "type": "button" },
"bindings": {
"onClick": {
"source": "onSelect,item.component.id",
"transform": "() => onSelect?.(item.component.id)"
},
"className": {
"source": "item.isSelected",
"transform": "item.isSelected ? 'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors bg-accent/40 text-foreground' : 'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors hover:bg-muted'"
},
"style": {
"source": "item.paddingLeft",
"transform": "{ paddingLeft: item.paddingLeft }"
}
},
"children": [
{
"type": "span",
"props": { "className": "font-medium" },
"bindings": { "children": "item.component.type" }
},
{
"type": "span",
"props": { "className": "text-xs text-muted-foreground" },
"bindings": { "children": "item.component.id" }
}
]
}
}
]
}
]
}

View File

@@ -0,0 +1,217 @@
{
"id": "seed-data-manager-card",
"type": "Card",
"children": [
{
"id": "card-header",
"type": "CardHeader",
"children": [
{
"id": "card-title",
"type": "CardTitle",
"props": { "className": "flex items-center gap-2" },
"children": [
{
"id": "title-icon",
"type": "PhosphorIcon",
"props": { "icon": "Database", "size": 24, "weight": "duotone" }
},
{
"id": "title-text",
"type": "text",
"bindings": { "children": "title" }
}
]
},
{
"id": "card-description",
"type": "CardDescription",
"bindings": { "children": "description" }
}
]
},
{
"id": "card-content",
"type": "CardContent",
"props": { "className": "flex flex-col gap-4" },
"children": [
{
"id": "loaded-alert",
"type": "Alert",
"conditional": { "if": "isLoaded" },
"children": [
{
"id": "alert-icon",
"type": "PhosphorIcon",
"props": { "icon": "CheckCircle", "className": "h-4 w-4", "weight": "fill" }
},
{
"id": "alert-description",
"type": "AlertDescription",
"children": [{ "type": "text", "children": "Seed data is loaded and available" }]
}
]
},
{
"id": "buttons-container",
"type": "div",
"props": { "className": "flex flex-col gap-3" },
"children": [
{
"id": "buttons-row",
"type": "div",
"props": { "className": "flex gap-2 flex-wrap" },
"children": [
{
"id": "load-button",
"type": "Button",
"bindings": {
"onClick": "onLoadSeedData",
"disabled": {
"source": "isLoading,isLoaded",
"transform": "isLoading || isLoaded"
}
},
"props": { "variant": "default" },
"children": [
{
"id": "load-button-content",
"type": "conditional-group",
"conditional": { "if": "isLoading" },
"children": [
{
"type": "PhosphorIcon",
"props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
},
{
"type": "text",
"bindings": { "children": "loadingLabel" }
}
],
"else": [
{
"type": "PhosphorIcon",
"props": { "icon": "Database", "size": 16, "weight": "fill" }
},
{
"type": "text",
"bindings": { "children": "loadLabel" }
}
]
}
]
},
{
"id": "reset-button",
"type": "Button",
"bindings": {
"onClick": "onResetSeedData",
"disabled": "isLoading"
},
"props": { "variant": "outline" },
"children": [
{
"id": "reset-button-content",
"type": "conditional-group",
"conditional": { "if": "isLoading" },
"children": [
{
"type": "PhosphorIcon",
"props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
},
{
"type": "text",
"bindings": { "children": "resettingLabel" }
}
],
"else": [
{
"type": "PhosphorIcon",
"props": { "icon": "ArrowClockwise", "size": 16, "weight": "bold" }
},
{
"type": "text",
"bindings": { "children": "resetLabel" }
}
]
}
]
},
{
"id": "clear-button",
"type": "Button",
"bindings": {
"onClick": "onClearAllData",
"disabled": "isLoading"
},
"props": { "variant": "destructive" },
"children": [
{
"id": "clear-button-content",
"type": "conditional-group",
"conditional": { "if": "isLoading" },
"children": [
{
"type": "PhosphorIcon",
"props": { "icon": "CircleNotch", "className": "animate-spin", "size": 16 }
},
{
"type": "text",
"bindings": { "children": "clearingLabel" }
}
],
"else": [
{
"type": "PhosphorIcon",
"props": { "icon": "Trash", "size": 16, "weight": "fill" }
},
{
"type": "text",
"bindings": { "children": "clearLabel" }
}
]
}
]
}
]
},
{
"id": "helper-text",
"type": "div",
"props": { "className": "text-sm text-muted-foreground space-y-1" },
"children": [
{
"id": "load-help",
"type": "p",
"conditional": { "if": "helperText.load" },
"children": [
{ "type": "strong", "children": "Load Seed Data: " },
{ "type": "text", "bindings": { "children": "helperText.load" } }
]
},
{
"id": "reset-help",
"type": "p",
"conditional": { "if": "helperText.reset" },
"children": [
{ "type": "strong", "children": "Reset to Defaults: " },
{ "type": "text", "bindings": { "children": "helperText.reset" } }
]
},
{
"id": "clear-help",
"type": "p",
"conditional": { "if": "helperText.clear" },
"children": [
{ "type": "strong", "children": "Clear All Data: " },
{ "type": "text", "bindings": { "children": "helperText.clear" } }
]
}
]
}
]
}
]
}
]
}

View File

@@ -1,55 +0,0 @@
import { cn } from '@/lib/utils'
import type { UIComponent } from '@/types/json-ui'
import type { ComponentTreeWrapperProps } from './interfaces'
const renderTreeNodes = (
components: UIComponent[],
depth: number,
selectedId: string | null,
onSelect?: (id: string) => void
) => {
return components.map((component) => {
const hasChildren = Array.isArray(component.children) && component.children.length > 0
const isSelected = selectedId === component.id
return (
<div key={component.id}>
<button
type="button"
onClick={() => onSelect?.(component.id)}
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors',
isSelected ? 'bg-accent/40 text-foreground' : 'hover:bg-muted'
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
>
<span className="font-medium">{component.type}</span>
<span className="text-xs text-muted-foreground">{component.id}</span>
</button>
{hasChildren && (
<div className="mt-1">
{renderTreeNodes(component.children as UIComponent[], depth + 1, selectedId, onSelect)}
</div>
)}
</div>
)
})
}
export function ComponentTreeWrapper({
components = [],
selectedId = null,
emptyMessage = 'No components available.',
onSelect,
className,
}: ComponentTreeWrapperProps) {
return (
<div className={cn('space-y-2', className)}>
{components.length === 0 ? (
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
) : (
<div className="space-y-1">{renderTreeNodes(components, 0, selectedId, onSelect)}</div>
)}
</div>
)
}

View File

@@ -1,122 +0,0 @@
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { CheckCircle, CircleNotch, Database, Trash, ArrowClockwise } from '@phosphor-icons/react'
import type { SeedDataManagerWrapperProps } from './interfaces'
export function SeedDataManagerWrapper({
isLoaded = false,
isLoading = false,
title = 'Seed Data Management',
description = 'Load, reset, or clear application seed data from the database',
loadLabel = 'Load Seed Data',
loadingLabel = 'Loading...',
resetLabel = 'Reset to Defaults',
resettingLabel = 'Resetting...',
clearLabel = 'Clear All Data',
clearingLabel = 'Clearing...',
onLoadSeedData,
onResetSeedData,
onClearAllData,
helperText = {
load: 'Populates database with initial data if not already loaded',
reset: 'Overwrites all data with fresh seed data',
clear: 'Removes all data from the database (destructive action)',
},
}: SeedDataManagerWrapperProps) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database size={24} weight="duotone" />
{title}
</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{isLoaded && (
<Alert>
<CheckCircle className="h-4 w-4" weight="fill" />
<AlertDescription>Seed data is loaded and available</AlertDescription>
</Alert>
)}
<div className="flex flex-col gap-3">
<div className="flex gap-2 flex-wrap">
<Button
onClick={onLoadSeedData}
disabled={isLoading || isLoaded}
variant="default"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{loadingLabel}
</>
) : (
<>
<Database size={16} weight="fill" />
{loadLabel}
</>
)}
</Button>
<Button
onClick={onResetSeedData}
disabled={isLoading}
variant="outline"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{resettingLabel}
</>
) : (
<>
<ArrowClockwise size={16} weight="bold" />
{resetLabel}
</>
)}
</Button>
<Button
onClick={onClearAllData}
disabled={isLoading}
variant="destructive"
>
{isLoading ? (
<>
<CircleNotch className="animate-spin" size={16} />
{clearingLabel}
</>
) : (
<>
<Trash size={16} weight="fill" />
{clearLabel}
</>
)}
</Button>
</div>
<div className="text-sm text-muted-foreground space-y-1">
{helperText.load && (
<p>
<strong>Load Seed Data:</strong> {helperText.load}
</p>
)}
{helperText.reset && (
<p>
<strong>Reset to Defaults:</strong> {helperText.reset}
</p>
)}
{helperText.clear && (
<p>
<strong>Clear All Data:</strong> {helperText.clear}
</p>
)}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -2,7 +2,6 @@ export { Breadcrumb } from './Breadcrumb'
export { CanvasRenderer } from './CanvasRenderer'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { ComponentPalette } from './ComponentPalette'
export { ComponentTree } from './ComponentTree'
export { GitHubBuildStatus } from './GitHubBuildStatus'
export { LazyLineChart } from './LazyLineChart'
export { LazyBarChart } from './LazyBarChart'
@@ -10,8 +9,6 @@ export { LazyD3BarChart } from './LazyD3BarChart'
export { StorageSettings } from './StorageSettings'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { PropertyEditor } from './PropertyEditor'
export { SaveIndicator } from './SaveIndicator'
export { SeedDataManager } from './SeedDataManager'
export { ToolbarButton } from './ToolbarButton'
export { TreeFormDialog } from './TreeFormDialog'
export { SearchInput } from './SearchInput'
@@ -25,6 +22,8 @@ export {
ComponentBindingDialog,
DataSourceEditorDialog,
GitHubBuildStatus as GitHubBuildStatusJSON,
SaveIndicator as SaveIndicatorJSON
SaveIndicator,
ComponentTree,
SeedDataManager
} from '@/lib/json-ui/json-components'
export { preloadMonacoEditor } from './LazyMonacoEditor'

View File

@@ -0,0 +1,46 @@
/**
* Hook for flattening and rendering component tree structure
* Converts recursive component tree to flat list with depth information
*/
import type { UIComponent } from '@/types/json-ui'
export interface TreeNode {
component: UIComponent
depth: number
hasChildren: boolean
isSelected: boolean
paddingLeft: string
}
export function useComponentTree(
components: UIComponent[],
selectedId: string | null
): TreeNode[] {
const flattenTree = (
items: UIComponent[],
depth: number = 0
): TreeNode[] => {
const result: TreeNode[] = []
for (const component of items) {
const hasChildren = Array.isArray(component.children) && component.children.length > 0
const isSelected = selectedId === component.id
result.push({
component,
depth,
hasChildren,
isSelected,
paddingLeft: `${depth * 16 + 8}px`
})
if (hasChildren) {
result.push(...flattenTree(component.children as UIComponent[], depth + 1))
}
}
return result
}
return flattenTree(components)
}

View File

@@ -0,0 +1,72 @@
/**
* Hook for calculating D3 bar chart dimensions and positions
*/
export interface BarChartData {
label: string
value: number
}
export interface BarPosition {
x: number
y: number
width: number
height: number
label: string
value: number
labelX: number
labelY: number
valueX: number
valueY: number
}
export interface ChartDimensions {
innerWidth: number
innerHeight: number
margin: { top: number; right: number; bottom: number; left: number }
translateX: number
translateY: number
bars: BarPosition[]
}
export function useD3BarChart(
data: BarChartData[],
width: number = 600,
height: number = 300
): ChartDimensions {
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const innerWidth = Math.max(width - margin.left - margin.right, 0)
const innerHeight = Math.max(height - margin.top - margin.bottom, 0)
const maxValue = Math.max(...data.map((item) => item.value), 0)
const barGap = 8
const barCount = data.length
const totalGap = barCount > 1 ? barGap * (barCount - 1) : 0
const barWidth = barCount > 0 ? Math.max((innerWidth - totalGap) / barCount, 0) : 0
const bars: BarPosition[] = data.map((item, index) => {
const barHeight = maxValue ? (item.value / maxValue) * innerHeight : 0
const x = index * (barWidth + barGap)
const y = innerHeight - barHeight
return {
x,
y,
width: barWidth,
height: barHeight,
label: item.label,
value: item.value,
labelX: x + barWidth / 2,
labelY: innerHeight + 16,
valueX: x + barWidth / 2,
valueY: Math.max(y - 6, 0)
}
})
return {
innerWidth,
innerHeight,
margin,
translateX: margin.left,
translateY: margin.top,
bars
}
}

View File

@@ -0,0 +1,30 @@
/**
* Hook for getting storage backend icon and copy
*/
import type { StorageBackendKey } from '@/components/storage/storageSettingsConfig'
import { getBackendCopy } from '@/components/storage/storageSettingsConfig'
export interface BackendInfo {
iconName: string
iconWeight: string
moleculeLabel: string
}
export function useStorageBackendInfo(backend: StorageBackendKey | null): BackendInfo {
const iconMap: Record<StorageBackendKey | 'null', { iconName: string; iconWeight: string }> = {
flask: { iconName: 'Cpu', iconWeight: 'regular' },
indexeddb: { iconName: 'HardDrive', iconWeight: 'regular' },
sqlite: { iconName: 'Database', iconWeight: 'regular' },
sparkkv: { iconName: 'Cloud', iconWeight: 'regular' },
null: { iconName: 'Database', iconWeight: 'regular' }
}
const icon = iconMap[backend || 'null']
const backendCopy = getBackendCopy(backend)
return {
iconName: icon.iconName,
iconWeight: icon.iconWeight,
moleculeLabel: backendCopy.moleculeLabel
}
}

View File

@@ -3,6 +3,9 @@
* Allows JSON components to use custom React hooks
*/
import { useSaveIndicator } from '@/hooks/use-save-indicator'
import { useComponentTree } from '@/hooks/use-component-tree'
import { useStorageBackendInfo } from '@/hooks/use-storage-backend-info'
import { useD3BarChart } from '@/hooks/use-d3-bar-chart'
export interface HookRegistry {
[key: string]: (...args: any[]) => any
@@ -13,6 +16,9 @@ export interface HookRegistry {
*/
export const hooksRegistry: HookRegistry = {
useSaveIndicator,
useComponentTree,
useStorageBackendInfo,
useD3BarChart,
// Add more hooks here as needed
}

View File

@@ -29,6 +29,8 @@ import componentBindingDialogDef from '@/components/json-definitions/component-b
import dataSourceEditorDialogDef from '@/components/json-definitions/data-source-editor-dialog.json'
import githubBuildStatusDef from '@/components/json-definitions/github-build-status.json'
import saveIndicatorDef from '@/components/json-definitions/save-indicator.json'
import componentTreeDef from '@/components/json-definitions/component-tree.json'
import seedDataManagerDef from '@/components/json-definitions/seed-data-manager.json'
// Create pure JSON components (no hooks)
export const LoadingFallback = createJsonComponent<LoadingFallbackProps>(loadingFallbackDef)
@@ -37,6 +39,7 @@ export const PageHeaderContent = createJsonComponent<PageHeaderContentProps>(pag
export const ComponentBindingDialog = createJsonComponent<ComponentBindingDialogProps>(componentBindingDialogDef)
export const DataSourceEditorDialog = createJsonComponent<DataSourceEditorDialogProps>(dataSourceEditorDialogDef)
export const GitHubBuildStatus = createJsonComponent<GitHubBuildStatusProps>(githubBuildStatusDef)
export const SeedDataManager = createJsonComponent<SeedDataManagerProps>(seedDataManagerDef)
// Create JSON components with hooks
export const SaveIndicator = createJsonComponentWithHooks<SaveIndicatorProps>(saveIndicatorDef, {
@@ -48,10 +51,17 @@ export const SaveIndicator = createJsonComponentWithHooks<SaveIndicatorProps>(sa
}
})
export const ComponentTree = createJsonComponentWithHooks<ComponentTreeProps>(componentTreeDef, {
hooks: {
treeData: {
hookName: 'useComponentTree',
args: (props) => [props.components || [], props.selectedId || null]
}
}
})
// Note: The following still need JSON definitions created:
// - LazyBarChart (needs Recharts integration)
// - LazyLineChart (needs Recharts integration)
// - LazyD3BarChart (needs D3 integration)
// - SeedDataManager (complex multi-button component)
// - StorageSettings (complex form component)
// - ComponentTree (needs recursive rendering support)
// - StorageSettings (complex form with backend switching)
// - LazyBarChart (Recharts integration)
// - LazyLineChart (Recharts integration)
// - LazyD3BarChart (D3 calculations - hook created, needs JSON definition)