mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
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:
3
frontends/nextjs/prisma-push.cjs
Normal file
3
frontends/nextjs/prisma-push.cjs
Normal 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' });
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user