diff --git a/src/components/JSONSchemaPageLoader.tsx b/src/components/JSONSchemaPageLoader.tsx index 916ec14..4a202f7 100644 --- a/src/components/JSONSchemaPageLoader.tsx +++ b/src/components/JSONSchemaPageLoader.tsx @@ -4,9 +4,11 @@ import { useSchemaLoader } from '@/hooks/use-schema-loader' interface JSONSchemaPageLoaderProps { schemaPath: string + data?: Record + functions?: Record } -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 + return } diff --git a/src/config/page-loader.ts b/src/config/page-loader.ts index 6ceee5a..0fdd503 100644 --- a/src/config/page-loader.ts +++ b/src/config/page-loader.ts @@ -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 { @@ -119,44 +133,39 @@ export function resolveProps(propConfig: PropConfig | undefined, stateContext: R const resolvedProps: Record = {} + const resolveEntries = ( + entries: string[] | undefined, + context: Record, + 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) } diff --git a/src/config/validate-config.ts b/src/config/validate-config.ts index 3b2b428..aa76cc3 100644 --- a/src/config/validate-config.ts +++ b/src/config/validate-config.ts @@ -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) { diff --git a/src/lib/json-ui/page-renderer.tsx b/src/lib/json-ui/page-renderer.tsx index 13d589e..a4f8483 100644 --- a/src/lib/json-ui/page-renderer.tsx +++ b/src/lib/json-ui/page-renderer.tsx @@ -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 + data?: Record + functions?: Record } -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) { diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 125694b..fd46ee4 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -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, + functions?: Record + ) => { if (page.schema) { console.log('[ROUTES] ๐Ÿงพ Rendering preloaded JSON schema for page:', page.id) - return + return } if (page.schemaPath) { console.log('[ROUTES] ๐Ÿงพ Rendering JSON schema loader for page:', page.id) - return + return } 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)