mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Add JSON page data/function prop mapping
This commit is contained in:
@@ -4,9 +4,11 @@ import { useSchemaLoader } from '@/hooks/use-schema-loader'
|
||||
|
||||
interface JSONSchemaPageLoaderProps {
|
||||
schemaPath: string
|
||||
data?: Record<string, any>
|
||||
functions?: Record<string, any>
|
||||
}
|
||||
|
||||
export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps) {
|
||||
export function JSONSchemaPageLoader({ schemaPath, data, functions }: JSONSchemaPageLoaderProps) {
|
||||
const { schema, loading, error } = useSchemaLoader(schemaPath)
|
||||
|
||||
if (loading) {
|
||||
@@ -21,5 +23,5 @@ export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps)
|
||||
)
|
||||
}
|
||||
|
||||
return <PageRenderer schema={schema} />
|
||||
return <PageRenderer schema={schema} data={data} functions={functions} />
|
||||
}
|
||||
|
||||
@@ -3,8 +3,22 @@ import { PageSchema } from '@/types/json-ui'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface PropConfig {
|
||||
/**
|
||||
* Component page prop bindings (map to stateContext).
|
||||
*/
|
||||
state?: string[]
|
||||
/**
|
||||
* Component page action bindings (map to actionContext).
|
||||
*/
|
||||
actions?: string[]
|
||||
/**
|
||||
* JSON page data bindings (map to stateContext).
|
||||
*/
|
||||
data?: string[]
|
||||
/**
|
||||
* JSON page function bindings (map to actionContext).
|
||||
*/
|
||||
functions?: string[]
|
||||
}
|
||||
|
||||
export interface ResizableConfig {
|
||||
@@ -107,44 +121,39 @@ export function resolveProps(propConfig: PropConfig | undefined, stateContext: R
|
||||
|
||||
const resolvedProps: Record<string, any> = {}
|
||||
|
||||
const resolveEntries = (
|
||||
entries: string[] | undefined,
|
||||
context: Record<string, any>,
|
||||
label: string
|
||||
) => {
|
||||
if (!entries?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[CONFIG] 📦 Resolving', entries.length, label)
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
const [propName, contextKey] = entry.includes(':')
|
||||
? entry.split(':')
|
||||
: [entry, entry]
|
||||
|
||||
if (context[contextKey] !== undefined) {
|
||||
resolvedProps[propName] = context[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved', label, 'prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️', label, 'prop not found:', contextKey)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve', label, 'prop:', entry, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (propConfig.state) {
|
||||
console.log('[CONFIG] 📦 Resolving', propConfig.state.length, 'state props')
|
||||
for (const stateKey of propConfig.state) {
|
||||
try {
|
||||
const [propName, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
if (stateContext[contextKey] !== undefined) {
|
||||
resolvedProps[propName] = stateContext[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved state prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️ State prop not found:', contextKey)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve state prop:', stateKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (propConfig.actions) {
|
||||
console.log('[CONFIG] 🎬 Resolving', propConfig.actions.length, 'action props')
|
||||
for (const actionKey of propConfig.actions) {
|
||||
try {
|
||||
const [propName, contextKey] = actionKey.split(':')
|
||||
|
||||
if (actionContext[contextKey]) {
|
||||
resolvedProps[propName] = actionContext[contextKey]
|
||||
console.log('[CONFIG] ✅ Resolved action prop:', propName)
|
||||
} else {
|
||||
console.log('[CONFIG] ⚠️ Action prop not found:', contextKey)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CONFIG] ❌ Failed to resolve action prop:', actionKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resolveEntries(propConfig.state, stateContext, 'state')
|
||||
resolveEntries(propConfig.data, stateContext, 'data')
|
||||
resolveEntries(propConfig.actions, actionContext, 'action')
|
||||
resolveEntries(propConfig.functions, actionContext, 'function')
|
||||
} catch (err) {
|
||||
console.error('[CONFIG] ❌ Failed to resolve props:', err)
|
||||
}
|
||||
|
||||
@@ -153,44 +153,53 @@ export function validatePageConfig(): ValidationError[] {
|
||||
}
|
||||
|
||||
if (page.props) {
|
||||
if (page.props.state) {
|
||||
page.props.state.forEach(stateKey => {
|
||||
const [, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
const validateStateKeys = (keys: string[] | undefined, field: string) => {
|
||||
if (!keys) return
|
||||
keys.forEach(stateKey => {
|
||||
const [, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
|
||||
if (!validStateKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.state',
|
||||
field,
|
||||
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (page.props.actions) {
|
||||
page.props.actions.forEach(actionKey => {
|
||||
const [, contextKey] = actionKey.split(':')
|
||||
|
||||
|
||||
const validateActionKeys = (keys: string[] | undefined, field: string) => {
|
||||
if (!keys) return
|
||||
keys.forEach(actionKey => {
|
||||
const [, contextKey] = actionKey.includes(':')
|
||||
? actionKey.split(':')
|
||||
: [actionKey, actionKey]
|
||||
|
||||
if (!contextKey) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.actions',
|
||||
field,
|
||||
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
|
||||
severity: 'error',
|
||||
})
|
||||
} else if (!validActionKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.actions',
|
||||
field,
|
||||
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
validateStateKeys(page.props.state, 'props.state')
|
||||
validateActionKeys(page.props.actions, 'props.actions')
|
||||
validateStateKeys(page.props.data, 'props.data')
|
||||
validateActionKeys(page.props.functions, 'props.functions')
|
||||
}
|
||||
|
||||
if (page.requiresResizable) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Action, PageSchema } from '@/types/json-ui'
|
||||
import { useDataSources } from '@/hooks/data/use-data-sources'
|
||||
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
||||
import { useAppSelector } from '@/store'
|
||||
@@ -8,20 +8,34 @@ import { ComponentRenderer } from './component-renderer'
|
||||
interface PageRendererProps {
|
||||
schema: PageSchema
|
||||
onCustomAction?: (action: any, event?: any) => Promise<void>
|
||||
data?: Record<string, any>
|
||||
functions?: Record<string, any>
|
||||
}
|
||||
|
||||
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
const { data, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
export function PageRenderer({ schema, onCustomAction, data: externalData, functions }: PageRendererProps) {
|
||||
const { data: sourceData, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
const state = useAppSelector((rootState) => rootState)
|
||||
const mergedData = useMemo(() => ({ ...sourceData, ...externalData }), [externalData, sourceData])
|
||||
const executeCustomAction = useCallback(async (action: Action, event?: any) => {
|
||||
if (onCustomAction) {
|
||||
await onCustomAction(action, event)
|
||||
return
|
||||
}
|
||||
|
||||
const handler = functions?.[action.id]
|
||||
if (typeof handler === 'function') {
|
||||
await handler(action, event)
|
||||
}
|
||||
}, [functions, onCustomAction])
|
||||
|
||||
const context = {
|
||||
data,
|
||||
const actionContext = {
|
||||
data: mergedData,
|
||||
updateData,
|
||||
updatePath,
|
||||
executeAction: onCustomAction || (async () => {}),
|
||||
executeAction: executeCustomAction,
|
||||
}
|
||||
|
||||
const { executeActions } = useActionExecutor(context)
|
||||
const { executeActions } = useActionExecutor(actionContext)
|
||||
|
||||
const handleEvent = useCallback((_componentId: string, handler: { actions: any[] }, eventData: any) => {
|
||||
if (!handler?.actions?.length) return
|
||||
@@ -34,7 +48,8 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
<ComponentRenderer
|
||||
key={component.id || index}
|
||||
component={component}
|
||||
data={data}
|
||||
data={mergedData}
|
||||
context={functions}
|
||||
state={state}
|
||||
onEvent={handleEvent}
|
||||
/>
|
||||
|
||||
@@ -89,15 +89,22 @@ export function createRoutes(
|
||||
const rootPage = enabledPages.find(p => p.isRoot)
|
||||
console.log('[ROUTES] 🏠 Root page search result:', rootPage ? `Found: ${rootPage.id} (${rootPage.component})` : 'NOT FOUND - will redirect to /dashboard')
|
||||
|
||||
const renderJsonPage = (page: typeof enabledPages[number]) => {
|
||||
// JSON page prop contract: page.props.data maps to stateContext -> data bindings,
|
||||
// page.props.functions maps to actionContext -> custom action handlers.
|
||||
// The mapping syntax matches props.state/props.actions (propName[:contextKey]).
|
||||
const renderJsonPage = (
|
||||
page: typeof enabledPages[number],
|
||||
data?: Record<string, any>,
|
||||
functions?: Record<string, any>
|
||||
) => {
|
||||
if (page.schema) {
|
||||
console.log('[ROUTES] 🧾 Rendering preloaded JSON schema for page:', page.id)
|
||||
return <PageRenderer schema={page.schema} />
|
||||
return <PageRenderer schema={page.schema} data={data} functions={functions} />
|
||||
}
|
||||
|
||||
if (page.schemaPath) {
|
||||
console.log('[ROUTES] 🧾 Rendering JSON schema loader for page:', page.id)
|
||||
return <JSONSchemaPageLoader schemaPath={page.schemaPath} />
|
||||
return <JSONSchemaPageLoader schemaPath={page.schemaPath} data={data} functions={functions} />
|
||||
}
|
||||
|
||||
console.error('[ROUTES] ❌ JSON page missing schemaPath:', page.id)
|
||||
@@ -114,9 +121,18 @@ export function createRoutes(
|
||||
: {}
|
||||
|
||||
if (page.type === 'json' || page.schemaPath) {
|
||||
const jsonDataConfig = page.props?.data ?? page.props?.state
|
||||
const jsonFunctionsConfig = page.props?.functions ?? page.props?.actions
|
||||
const jsonData = jsonDataConfig
|
||||
? resolveProps({ state: jsonDataConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
const jsonFunctions = jsonFunctionsConfig
|
||||
? resolveProps({ actions: jsonFunctionsConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
return {
|
||||
path: `/${page.id}`,
|
||||
element: renderJsonPage(page)
|
||||
element: renderJsonPage(page, jsonData, jsonFunctions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +184,18 @@ export function createRoutes(
|
||||
: {}
|
||||
|
||||
if (rootPage.type === 'json' || rootPage.schemaPath) {
|
||||
const jsonDataConfig = rootPage.props?.data ?? rootPage.props?.state
|
||||
const jsonFunctionsConfig = rootPage.props?.functions ?? rootPage.props?.actions
|
||||
const jsonData = jsonDataConfig
|
||||
? resolveProps({ state: jsonDataConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
const jsonFunctions = jsonFunctionsConfig
|
||||
? resolveProps({ actions: jsonFunctionsConfig }, stateContext, actionContext)
|
||||
: {}
|
||||
|
||||
routes.push({
|
||||
path: '/',
|
||||
element: renderJsonPage(rootPage)
|
||||
element: renderJsonPage(rootPage, jsonData, jsonFunctions)
|
||||
})
|
||||
} else if (!rootPage.component) {
|
||||
console.error('[ROUTES] ❌ Root page missing component:', rootPage.id)
|
||||
|
||||
Reference in New Issue
Block a user