Merge pull request #1461 from johndoe6345789/copilot/implement-working-front-page

Implement $ref resolution in JSON component renderer to enable declarative page composition
This commit is contained in:
2026-01-16 18:12:25 +00:00
committed by GitHub
4 changed files with 198 additions and 14 deletions

View File

@@ -0,0 +1,3 @@
const { execSync } = require('child_process');
process.env.DATABASE_URL = process.env.DATABASE_URL || 'file:./dev.db';
execSync('npx prisma db push --schema=../../dbal/shared/prisma/schema.prisma', { stdio: 'inherit' });

View File

@@ -79,7 +79,7 @@ export default async function RootPage() {
const component = pkg.components?.find(c => c.id === route.component || c.name === route.component)
if (component !== undefined) {
return renderJSONComponent(component, {}, {})
return renderJSONComponent(component, {}, {}, pkg.components)
}
} catch {
// Package doesn't exist or can't be loaded, fall through
@@ -116,7 +116,7 @@ export default async function RootPage() {
) ?? pkg.components[0]
if (pageComponent !== undefined) {
return renderJSONComponent(pageComponent, {}, {})
return renderJSONComponent(pageComponent, {}, {}, pkg.components)
}
}
} catch {

View File

@@ -22,11 +22,13 @@ export interface RenderContext {
*
* By default, uses the FAKEMUI_REGISTRY to render components.
* Pass a custom ComponentRegistry to override specific components.
* Pass allComponents to enable $ref resolution within the same package.
*/
export function renderJSONComponent(
component: JSONComponent,
props: Record<string, JsonValue> = {},
ComponentRegistry: Record<string, React.ComponentType<Record<string, unknown>>> = FAKEMUI_REGISTRY
ComponentRegistry: Record<string, React.ComponentType<Record<string, unknown>>> = FAKEMUI_REGISTRY,
allComponents?: JSONComponent[]
): React.ReactElement {
if (component.render === undefined) {
return (
@@ -36,6 +38,11 @@ export function renderJSONComponent(
)
}
// Build component registry for $ref resolution
const componentRegistry = allComponents
? new Map(allComponents.map(c => [c.id, c]))
: undefined
const context: RenderContext = {
props,
state: {},
@@ -50,7 +57,7 @@ export function renderJSONComponent(
</div>
)
}
return renderTemplate(template, context, ComponentRegistry)
return renderTemplate(template, context, ComponentRegistry, componentRegistry)
} catch (error) {
return (
<div style={{ padding: '1rem', border: '1px solid red', borderRadius: '0.25rem' }}>
@@ -67,7 +74,8 @@ export function renderJSONComponent(
function renderTemplate(
node: JsonValue,
context: RenderContext,
ComponentRegistry: Record<string, React.ComponentType<Record<string, unknown>>>
ComponentRegistry: Record<string, React.ComponentType<Record<string, unknown>>>,
componentRegistry?: Map<string, JSONComponent>
): React.ReactElement {
if (node === null || typeof node !== 'object') {
return <>{String(node)}</>
@@ -81,6 +89,20 @@ function renderTemplate(
// Now TypeScript knows it's a JsonObject (non-array object)
const nodeObj = node as Record<string, JsonValue>
// Handle $ref to other components in the same package
if (typeof nodeObj.$ref === 'string' && componentRegistry !== undefined) {
const referencedComponent = componentRegistry.get(nodeObj.$ref)
if (referencedComponent !== undefined && referencedComponent.render?.template !== undefined) {
return renderTemplate(referencedComponent.render.template, context, ComponentRegistry, componentRegistry)
} else {
return (
<div style={{ padding: '0.5rem', border: '1px dashed orange', borderRadius: '0.25rem' }}>
<strong>Warning:</strong> Component reference "${nodeObj.$ref}" not found
</div>
)
}
}
// Handle conditional rendering
if (nodeObj.type === 'conditional') {
const conditionValue = nodeObj.condition
@@ -90,9 +112,9 @@ function renderTemplate(
const condition = evaluateExpression(conditionValue, context)
const conditionIsTrue = condition !== null && condition !== undefined && condition !== false && condition !== 0 && condition !== ''
if (conditionIsTrue && nodeObj.then !== null && nodeObj.then !== undefined) {
return renderTemplate(nodeObj.then, context, ComponentRegistry)
return renderTemplate(nodeObj.then, context, ComponentRegistry, componentRegistry)
} else if (!conditionIsTrue && nodeObj.else !== null && nodeObj.else !== undefined) {
return renderTemplate(nodeObj.else, context, ComponentRegistry)
return renderTemplate(nodeObj.else, context, ComponentRegistry, componentRegistry)
}
return <></>
}
@@ -128,12 +150,12 @@ function renderTemplate(
}
return (
<React.Fragment key={index}>
{renderTemplate(child, context, ComponentRegistry)}
{renderTemplate(child, context, ComponentRegistry, componentRegistry)}
</React.Fragment>
)
})
} else {
children = renderTemplate(nodeChildren, context, ComponentRegistry)
children = renderTemplate(nodeChildren, context, ComponentRegistry, componentRegistry)
}
}
@@ -189,12 +211,12 @@ function renderTemplate(
}
return (
<React.Fragment key={index}>
{renderTemplate(child, context, ComponentRegistry)}
{renderTemplate(child, context, ComponentRegistry, componentRegistry)}
</React.Fragment>
)
})
} else {
children = renderTemplate(nodeChildren, context, ComponentRegistry)
children = renderTemplate(nodeChildren, context, ComponentRegistry, componentRegistry)
}
}

View File

@@ -13,17 +13,19 @@
"type": "element",
"template": {
"type": "Box",
"component": "main",
"component": "div",
"className": "home-page",
"sx": {
"minHeight": "100vh",
"background": "linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.05) 0%, transparent 50%, rgba(var(--color-accent-rgb), 0.05) 100%)"
},
"children": [
{ "$ref": "landing_nav" },
{ "$ref": "hero_section" },
{ "$ref": "features_section" },
{ "$ref": "about_section" },
{ "$ref": "contact_section" }
{ "$ref": "contact_section" },
{ "$ref": "landing_footer" }
]
}
}
@@ -620,6 +622,161 @@
]
}
}
},
{
"id": "landing_nav",
"name": "LandingNav",
"description": "Navigation bar for landing page with logo, links, and mobile menu",
"props": [],
"render": {
"type": "element",
"template": {
"type": "Box",
"component": "nav",
"className": "landing-nav",
"sx": {
"borderBottom": 1,
"borderColor": "divider",
"bgcolor": "rgba(255, 255, 255, 0.5)",
"backdropFilter": "blur(8px)",
"position": "sticky",
"top": 0,
"zIndex": 50
},
"children": [
{
"type": "Container",
"maxWidth": "lg",
"children": [
{
"type": "Stack",
"direction": "row",
"justifyContent": "space-between",
"alignItems": "center",
"sx": { "height": 64 },
"children": [
{
"type": "Stack",
"direction": "row",
"alignItems": "center",
"spacing": 1.5,
"children": [
{
"type": "Box",
"sx": {
"width": 32,
"height": 32,
"borderRadius": 2,
"background": "linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%)"
}
},
{
"type": "Text",
"variant": "h6",
"children": "MetaBuilder",
"sx": { "fontWeight": 700 }
}
]
},
{
"type": "Stack",
"direction": "row",
"spacing": 2,
"sx": { "display": { "xs": "none", "md": "flex" } },
"children": [
{
"type": "Link",
"href": "#features",
"children": "Features",
"sx": {
"color": "text.secondary",
"textDecoration": "none",
"fontSize": "0.875rem",
"&:hover": { "color": "text.primary" }
}
},
{
"type": "Link",
"href": "#about",
"children": "About",
"sx": {
"color": "text.secondary",
"textDecoration": "none",
"fontSize": "0.875rem",
"&:hover": { "color": "text.primary" }
}
},
{
"type": "Link",
"href": "#contact",
"children": "Contact",
"sx": {
"color": "text.secondary",
"textDecoration": "none",
"fontSize": "0.875rem",
"&:hover": { "color": "text.primary" }
}
},
{
"type": "Button",
"variant": "outlined",
"size": "small",
"children": "Sign In",
"href": "/ui/login"
},
{
"type": "Button",
"variant": "contained",
"size": "small",
"children": "Admin"
}
]
}
]
}
]
}
]
}
}
},
{
"id": "landing_footer",
"name": "LandingFooter",
"description": "Simple footer for landing page",
"props": [],
"render": {
"type": "element",
"template": {
"type": "Box",
"component": "footer",
"className": "landing-footer",
"sx": {
"borderTop": 1,
"borderColor": "divider",
"bgcolor": "action.hover",
"py": 4,
"mt": 10
},
"children": [
{
"type": "Container",
"maxWidth": "lg",
"children": [
{
"type": "Text",
"variant": "body2",
"sx": {
"color": "text.secondary",
"textAlign": "center"
},
"children": "© 2025 MetaBuilder. Built with the power of visual workflows and declarative schemas."
}
]
}
]
}
}
}
],
"exports": {
@@ -634,7 +791,9 @@
"FeatureCard5",
"FeatureCard6",
"AboutSection",
"ContactSection"
"ContactSection",
"LandingNav",
"LandingFooter"
]
}
}