mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
feat: Implement hooks loader system for JSON components, convert SaveIndicator to pure JSON
- Created hooks-registry.ts for registering custom React hooks - Created createJsonComponentWithHooks for JSON components that need hooks - Implemented SaveIndicator as pure JSON with useSaveIndicator hook - Moved JSON definitions from wrappers/definitions to components/json-definitions - Removed wrappers folder entirely - Fixed NavigationItem JSON to include onClick handler binding - Deleted legacy NavigationItem.tsx and PageHeaderContent.tsx files - Architecture: JSON + interfaces + hook loader = fully functional components Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
"className": {
|
||||
"source": "isActive",
|
||||
"transform": "data ? 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors bg-primary text-primary-foreground' : 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors hover:bg-muted text-foreground'"
|
||||
}
|
||||
},
|
||||
"onClick": "onClick"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
42
src/components/json-definitions/save-indicator.json
Normal file
42
src/components/json-definitions/save-indicator.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "save-indicator-container",
|
||||
"type": "div",
|
||||
"bindings": {
|
||||
"className": {
|
||||
"source": "className",
|
||||
"transform": "data ? `flex items-center gap-1.5 text-xs text-muted-foreground ${data}` : 'flex items-center gap-1.5 text-xs text-muted-foreground'"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "status-icon",
|
||||
"type": "StatusIcon",
|
||||
"bindings": {
|
||||
"type": {
|
||||
"source": "lastSaved",
|
||||
"transform": "data ? (hookData.isRecent ? 'saved' : 'synced') : status"
|
||||
},
|
||||
"animate": {
|
||||
"source": "animate",
|
||||
"transform": "data !== undefined ? data : (lastSaved ? hookData.isRecent : status === 'saved')"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "label-text",
|
||||
"type": "span",
|
||||
"props": {
|
||||
"className": "hidden sm:inline"
|
||||
},
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "label",
|
||||
"transform": "data || (lastSaved ? (hookData.isRecent ? 'Saved' : hookData.timeAgo) : (status === 'saved' ? 'Saved' : 'Synced'))"
|
||||
}
|
||||
},
|
||||
"conditional": {
|
||||
"if": "showLabel !== false"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Badge, Flex, Text, IconWrapper } from '@/components/atoms'
|
||||
|
||||
interface NavigationItemProps {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
isActive: boolean
|
||||
badge?: number
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function NavigationItem({
|
||||
icon,
|
||||
label,
|
||||
isActive,
|
||||
badge,
|
||||
onClick,
|
||||
}: NavigationItemProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<IconWrapper
|
||||
icon={icon}
|
||||
size="md"
|
||||
variant={isActive ? 'default' : 'muted'}
|
||||
/>
|
||||
<Text className="flex-1 text-left font-medium" variant="small">
|
||||
{label}
|
||||
</Text>
|
||||
{badge !== undefined && badge > 0 && (
|
||||
<Badge
|
||||
variant={isActive ? 'secondary' : 'destructive'}
|
||||
className="ml-auto"
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { TabIcon } from '@/components/atoms'
|
||||
|
||||
interface PageHeaderContentProps {
|
||||
title: string
|
||||
icon: React.ReactNode
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function PageHeaderContent({ title, icon, description }: PageHeaderContentProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<TabIcon icon={icon} variant="gradient" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg sm:text-xl font-bold truncate">{title}</h2>
|
||||
{description && (
|
||||
<p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
src/lib/json-ui/create-json-component-with-hooks.tsx
Normal file
47
src/lib/json-ui/create-json-component-with-hooks.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ComponentRenderer } from './component-renderer'
|
||||
import { getHook } from './hooks-registry'
|
||||
|
||||
/**
|
||||
* Creates a React component from a JSON definition with hook support
|
||||
* Allows JSON components to use custom React hooks
|
||||
*/
|
||||
export function createJsonComponentWithHooks<TProps = any>(
|
||||
jsonDefinition: any,
|
||||
options?: {
|
||||
hooks?: {
|
||||
[key: string]: {
|
||||
hookName: string
|
||||
args?: (props: TProps) => any[]
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
return function JsonComponent(props: TProps) {
|
||||
// Execute hooks if defined
|
||||
const hookResults: Record<string, any> = {}
|
||||
|
||||
if (options?.hooks) {
|
||||
for (const [resultKey, hookConfig] of Object.entries(options.hooks)) {
|
||||
const hook = getHook(hookConfig.hookName)
|
||||
if (hook) {
|
||||
const args = hookConfig.args ? hookConfig.args(props) : []
|
||||
hookResults[resultKey] = hook(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge hook results with props for data binding
|
||||
const dataWithHooks = {
|
||||
...props,
|
||||
...hookResults,
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentRenderer
|
||||
component={jsonDefinition}
|
||||
data={dataWithHooks}
|
||||
context={{}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
31
src/lib/json-ui/hooks-registry.ts
Normal file
31
src/lib/json-ui/hooks-registry.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Hook Registry for JSON Components
|
||||
* Allows JSON components to use custom React hooks
|
||||
*/
|
||||
import { useSaveIndicator } from '@/hooks/use-save-indicator'
|
||||
|
||||
export interface HookRegistry {
|
||||
[key: string]: (...args: any[]) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all custom hooks available to JSON components
|
||||
*/
|
||||
export const hooksRegistry: HookRegistry = {
|
||||
useSaveIndicator,
|
||||
// Add more hooks here as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hook from the registry by name
|
||||
*/
|
||||
export function getHook(hookName: string) {
|
||||
return hooksRegistry[hookName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new hook
|
||||
*/
|
||||
export function registerHook(name: string, hook: (...args: any[]) => any) {
|
||||
hooksRegistry[name] = hook
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './loading-fallback'
|
||||
export * from './navigation-item'
|
||||
export * from './page-header-content'
|
||||
export * from './wrapper-interfaces'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { UIComponent } from '@/types/json-ui'
|
||||
|
||||
export type SaveIndicatorStatus = 'saved' | 'synced'
|
||||
|
||||
export interface SaveIndicatorWrapperProps {
|
||||
export interface SaveIndicatorProps {
|
||||
lastSaved?: number | null
|
||||
status?: SaveIndicatorStatus
|
||||
label?: string
|
||||
@@ -12,7 +12,7 @@ export interface SaveIndicatorWrapperProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface LazyBarChartWrapperProps {
|
||||
export interface LazyBarChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
@@ -25,7 +25,7 @@ export interface LazyBarChartWrapperProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface LazyLineChartWrapperProps {
|
||||
export interface LazyLineChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
@@ -38,7 +38,7 @@ export interface LazyLineChartWrapperProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface LazyD3BarChartWrapperProps {
|
||||
export interface LazyD3BarChartProps {
|
||||
data: Array<{ label: string; value: number }>
|
||||
width?: number
|
||||
height?: number
|
||||
@@ -46,7 +46,7 @@ export interface LazyD3BarChartWrapperProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface SeedDataManagerWrapperProps {
|
||||
export interface SeedDataManagerProps {
|
||||
isLoaded?: boolean
|
||||
isLoading?: boolean
|
||||
title?: string
|
||||
@@ -67,7 +67,7 @@ export interface SeedDataManagerWrapperProps {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StorageSettingsWrapperProps {
|
||||
export interface StorageSettingsProps {
|
||||
backend?: StorageBackendKey | null
|
||||
isLoading?: boolean
|
||||
flaskUrl?: string
|
||||
@@ -93,7 +93,7 @@ export interface GitHubBuildStatusWorkflowItem {
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface GitHubBuildStatusWrapperProps {
|
||||
export interface GitHubBuildStatusProps {
|
||||
title?: string
|
||||
description?: string
|
||||
workflows?: GitHubBuildStatusWorkflowItem[]
|
||||
@@ -112,7 +112,7 @@ export interface ComponentBindingField {
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export interface ComponentBindingDialogWrapperProps {
|
||||
export interface ComponentBindingDialogProps {
|
||||
open?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
@@ -134,7 +134,7 @@ export interface DataSourceField {
|
||||
helperText?: string
|
||||
}
|
||||
|
||||
export interface DataSourceEditorDialogWrapperProps {
|
||||
export interface DataSourceEditorDialogProps {
|
||||
open?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
@@ -146,7 +146,7 @@ export interface DataSourceEditorDialogWrapperProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface ComponentTreeWrapperProps {
|
||||
export interface ComponentTreeProps {
|
||||
components?: UIComponent[]
|
||||
selectedId?: string | null
|
||||
emptyMessage?: string
|
||||
@@ -1,16 +1,57 @@
|
||||
/**
|
||||
* Pure JSON components - no TypeScript wrappers needed
|
||||
* Interfaces are defined in src/lib/json-ui/interfaces/
|
||||
* JSON definitions are in src/components/json-definitions/
|
||||
*/
|
||||
import { createJsonComponent } from './create-json-component'
|
||||
import type { LoadingFallbackProps } from './interfaces/loading-fallback'
|
||||
import type { NavigationItemProps } from './interfaces/navigation-item'
|
||||
import type { PageHeaderContentProps } from './interfaces/page-header-content'
|
||||
import { createJsonComponentWithHooks } from './create-json-component-with-hooks'
|
||||
import type {
|
||||
LoadingFallbackProps,
|
||||
NavigationItemProps,
|
||||
PageHeaderContentProps,
|
||||
SaveIndicatorProps,
|
||||
LazyBarChartProps,
|
||||
LazyLineChartProps,
|
||||
LazyD3BarChartProps,
|
||||
SeedDataManagerProps,
|
||||
StorageSettingsProps,
|
||||
GitHubBuildStatusProps,
|
||||
ComponentBindingDialogProps,
|
||||
DataSourceEditorDialogProps,
|
||||
ComponentTreeProps,
|
||||
} from './interfaces'
|
||||
|
||||
import loadingFallbackDef from './wrappers/definitions/loading-fallback.json'
|
||||
import navigationItemDef from './wrappers/definitions/navigation-item.json'
|
||||
import pageHeaderContentDef from './wrappers/definitions/page-header-content.json'
|
||||
// Import JSON definitions
|
||||
import loadingFallbackDef from '@/components/json-definitions/loading-fallback.json'
|
||||
import navigationItemDef from '@/components/json-definitions/navigation-item.json'
|
||||
import pageHeaderContentDef from '@/components/json-definitions/page-header-content.json'
|
||||
import componentBindingDialogDef from '@/components/json-definitions/component-binding-dialog.json'
|
||||
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'
|
||||
|
||||
// Create pure JSON components (no hooks)
|
||||
export const LoadingFallback = createJsonComponent<LoadingFallbackProps>(loadingFallbackDef)
|
||||
export const NavigationItem = createJsonComponent<NavigationItemProps>(navigationItemDef)
|
||||
export const PageHeaderContent = createJsonComponent<PageHeaderContentProps>(pageHeaderContentDef)
|
||||
export const ComponentBindingDialog = createJsonComponent<ComponentBindingDialogProps>(componentBindingDialogDef)
|
||||
export const DataSourceEditorDialog = createJsonComponent<DataSourceEditorDialogProps>(dataSourceEditorDialogDef)
|
||||
export const GitHubBuildStatus = createJsonComponent<GitHubBuildStatusProps>(githubBuildStatusDef)
|
||||
|
||||
// Create JSON components with hooks
|
||||
export const SaveIndicator = createJsonComponentWithHooks<SaveIndicatorProps>(saveIndicatorDef, {
|
||||
hooks: {
|
||||
hookData: {
|
||||
hookName: 'useSaveIndicator',
|
||||
args: (props) => [props.lastSaved ?? 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)
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { ComponentRenderer } from '@/lib/json-ui/component-renderer'
|
||||
import { cn } from '@/lib/utils'
|
||||
import gitHubBuildStatusDefinition from './definitions/github-build-status.json'
|
||||
import type { GitHubBuildStatusWrapperProps, GitHubBuildStatusWorkflowItem } from './interfaces'
|
||||
|
||||
const getWorkflowStatus = (workflow: GitHubBuildStatusWorkflowItem) => {
|
||||
if (workflow.status === 'completed') {
|
||||
if (workflow.conclusion === 'success') {
|
||||
return {
|
||||
label: 'Success',
|
||||
className: 'bg-green-500/10 text-green-600 border-green-500/20',
|
||||
}
|
||||
}
|
||||
if (workflow.conclusion === 'failure') {
|
||||
return { label: 'Failed', className: 'bg-red-500/10 text-red-600 border-red-500/20' }
|
||||
}
|
||||
if (workflow.conclusion === 'cancelled') {
|
||||
return { label: 'Cancelled', className: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' }
|
||||
}
|
||||
}
|
||||
|
||||
return { label: 'Running', className: 'border-blue-500/50 text-blue-500' }
|
||||
}
|
||||
|
||||
export function GitHubBuildStatusWrapper({
|
||||
title = 'GitHub Build Status',
|
||||
description = 'Latest workflow runs and status badges.',
|
||||
workflows = [],
|
||||
isLoading = false,
|
||||
errorMessage,
|
||||
emptyMessage = 'No workflows to display yet.',
|
||||
footerLinkLabel = 'View on GitHub',
|
||||
footerLinkUrl,
|
||||
className,
|
||||
}: GitHubBuildStatusWrapperProps) {
|
||||
const normalizedWorkflows = workflows.map((workflow) => {
|
||||
const status = getWorkflowStatus(workflow)
|
||||
return {
|
||||
...workflow,
|
||||
statusLabel: status.label,
|
||||
statusClass: status.className,
|
||||
summaryLine: [workflow.branch, workflow.updatedAt, workflow.event].filter(Boolean).join(' • '),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<ComponentRenderer
|
||||
component={gitHubBuildStatusDefinition}
|
||||
data={{
|
||||
title,
|
||||
description,
|
||||
workflows: normalizedWorkflows,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
emptyMessage,
|
||||
loadingMessage: 'Loading workflows…',
|
||||
hasWorkflows: normalizedWorkflows.length > 0,
|
||||
footerLinkLabel,
|
||||
footerLinkUrl,
|
||||
className: cn(className),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { LazyBarChartWrapperProps } from './interfaces'
|
||||
|
||||
export function LazyBarChartWrapper({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = '100%',
|
||||
height = 300,
|
||||
color = '#8884d8',
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
showLegend = true,
|
||||
className,
|
||||
}: LazyBarChartWrapperProps) {
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<BarChart data={data}>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
{showTooltip && <Tooltip />}
|
||||
{showLegend && <Legend />}
|
||||
<Bar dataKey={yKey} fill={color} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { LazyLineChartWrapperProps } from './interfaces'
|
||||
|
||||
export function LazyLineChartWrapper({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = '100%',
|
||||
height = 300,
|
||||
color = '#8884d8',
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
showLegend = true,
|
||||
className,
|
||||
}: LazyLineChartWrapperProps) {
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<LineChart data={data}>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
{showTooltip && <Tooltip />}
|
||||
{showLegend && <Legend />}
|
||||
<Line type="monotone" dataKey={yKey} stroke={color} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { StatusIcon } from '@/components/atoms'
|
||||
import { useSaveIndicator } from '@/hooks/use-save-indicator'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SaveIndicatorWrapperProps } from './interfaces'
|
||||
|
||||
export function SaveIndicatorWrapper({
|
||||
lastSaved,
|
||||
status = 'saved',
|
||||
label,
|
||||
showLabel = true,
|
||||
animate,
|
||||
className,
|
||||
}: SaveIndicatorWrapperProps) {
|
||||
const { timeAgo, isRecent } = useSaveIndicator(lastSaved ?? null)
|
||||
|
||||
if (lastSaved) {
|
||||
const resolvedStatus = isRecent ? 'saved' : 'synced'
|
||||
const resolvedLabel = label ?? (isRecent ? 'Saved' : timeAgo)
|
||||
const shouldAnimate = animate ?? isRecent
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}>
|
||||
<StatusIcon type={resolvedStatus} animate={shouldAnimate} />
|
||||
{showLabel && <span className="hidden sm:inline">{resolvedLabel}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedLabel = label ?? (status === 'saved' ? 'Saved' : 'Synced')
|
||||
const shouldAnimate = animate ?? status === 'saved'
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}>
|
||||
<StatusIcon type={status} animate={shouldAnimate} />
|
||||
{showLabel && <span className="hidden sm:inline">{resolvedLabel}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export { SaveIndicatorWrapper } from './SaveIndicatorWrapper'
|
||||
export { LazyBarChartWrapper } from './LazyBarChartWrapper'
|
||||
export { LazyLineChartWrapper } from './LazyLineChartWrapper'
|
||||
export { LazyD3BarChartWrapper } from './LazyD3BarChartWrapper'
|
||||
export { SeedDataManagerWrapper } from './SeedDataManagerWrapper'
|
||||
export { StorageSettingsWrapper } from './StorageSettingsWrapper'
|
||||
export { GitHubBuildStatusWrapper } from './GitHubBuildStatusWrapper'
|
||||
export { ComponentBindingDialogWrapper } from './ComponentBindingDialogWrapper'
|
||||
export { DataSourceEditorDialogWrapper } from './DataSourceEditorDialogWrapper'
|
||||
export { ComponentTreeWrapper } from './ComponentTreeWrapper'
|
||||
// LoadingFallbackWrapper, NavigationItemWrapper, PageHeaderContentWrapper removed
|
||||
// These are now pure JSON components exported from @/lib/json-ui/json-components
|
||||
Reference in New Issue
Block a user