Add ComponentTreeRenderer and expand features.json with tab component trees

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 13:59:56 +00:00
parent d65794e0ad
commit 4233aadc3f
2 changed files with 611 additions and 0 deletions

View File

@@ -1133,6 +1133,347 @@
]
}
]
},
"TableManagerTab": {
"component": "Box",
"children": [
{
"component": "Typography",
"props": {
"variant": "h5",
"gutterBottom": true,
"text": "{{feature.name}}"
}
},
{
"component": "Typography",
"condition": "feature.description",
"props": {
"variant": "body2",
"color": "text.secondary",
"gutterBottom": true,
"text": "{{feature.description}}"
}
},
{
"component": "Box",
"props": {
"sx": { "mt": 2, "mb": 2 }
},
"children": [
{
"component": "Button",
"condition": "canCreate",
"props": {
"variant": "contained",
"startIcon": "Add",
"onClick": "openCreateDialog",
"sx": { "mr": 2 },
"text": "Create Table"
}
},
{
"component": "Button",
"condition": "canDelete",
"props": {
"variant": "outlined",
"color": "error",
"startIcon": "Delete",
"onClick": "openDropDialog",
"text": "Drop Table"
}
}
]
},
{
"component": "Paper",
"props": {
"sx": { "mt": 2 }
},
"children": [
{
"component": "Box",
"props": {
"sx": { "p": 2 }
},
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"gutterBottom": true,
"text": "Existing Tables"
}
},
{
"component": "List",
"children": [
{
"component": "ListItem",
"forEach": "tables",
"children": [
{
"component": "ListItemIcon",
"children": [
{
"component": "IconButton",
"props": {
"size": "small"
},
"children": [
{
"component": "Typography",
"props": {
"text": "📊"
}
}
]
}
]
},
{
"component": "ListItemText",
"props": {
"primary": "{{table.table_name}}"
}
}
]
},
{
"component": "ListItem",
"condition": "tables.length === 0",
"children": [
{
"component": "ListItemText",
"props": {
"primary": "No tables found"
}
}
]
}
]
}
]
}
]
}
]
},
"ColumnManagerTab": {
"component": "Box",
"children": [
{
"component": "Typography",
"props": {
"variant": "h5",
"gutterBottom": true,
"text": "{{feature.name}}"
}
},
{
"component": "Typography",
"condition": "feature.description",
"props": {
"variant": "body2",
"color": "text.secondary",
"gutterBottom": true,
"text": "{{feature.description}}"
}
},
{
"component": "Paper",
"props": {
"sx": { "p": 2, "mb": 2 }
},
"children": [
{
"component": "FormControl",
"props": {
"fullWidth": true
},
"children": [
{
"component": "InputLabel",
"props": {
"text": "Select Table"
}
},
{
"component": "Select",
"props": {
"value": "{{selectedTable}}",
"label": "Select Table",
"onChange": "handleTableChange"
},
"children": [
{
"component": "MenuItem",
"forEach": "tables",
"props": {
"value": "{{table.table_name}}",
"text": "{{table.table_name}}"
}
}
]
}
]
}
]
},
{
"component": "Box",
"condition": "selectedTable && canAdd",
"props": {
"sx": { "mb": 2 }
},
"children": [
{
"component": "Button",
"condition": "canAdd",
"props": {
"variant": "contained",
"startIcon": "Add",
"onClick": "openAddDialog",
"sx": { "mr": 1 },
"text": "Add Column"
}
},
{
"component": "Button",
"condition": "canModify",
"props": {
"variant": "outlined",
"startIcon": "Edit",
"onClick": "openModifyDialog",
"sx": { "mr": 1 },
"text": "Modify Column"
}
},
{
"component": "Button",
"condition": "canDelete",
"props": {
"variant": "outlined",
"color": "error",
"startIcon": "Delete",
"onClick": "openDropDialog",
"text": "Drop Column"
}
}
]
},
{
"component": "Paper",
"condition": "tableSchema && tableSchema.columns",
"props": {
"sx": { "mt": 2 }
},
"children": [
{
"component": "TableContainer",
"children": [
{
"component": "Table",
"props": {
"size": "small"
},
"children": [
{
"component": "TableHead",
"children": [
{
"component": "TableRow",
"children": [
{
"component": "TableCell",
"children": [
{
"component": "Typography",
"props": {
"text": "Column Name"
}
}
]
},
{
"component": "TableCell",
"children": [
{
"component": "Typography",
"props": {
"text": "Data Type"
}
}
]
},
{
"component": "TableCell",
"children": [
{
"component": "Typography",
"props": {
"text": "Nullable"
}
}
]
},
{
"component": "TableCell",
"children": [
{
"component": "Typography",
"props": {
"text": "Default"
}
}
]
}
]
}
]
},
{
"component": "TableBody",
"children": [
{
"component": "TableRow",
"forEach": "tableSchema.columns",
"children": [
{
"component": "TableCell",
"props": {
"text": "{{column.column_name}}"
}
},
{
"component": "TableCell",
"props": {
"text": "{{column.data_type}}"
}
},
{
"component": "TableCell",
"props": {
"text": "{{column.is_nullable}}"
}
},
{
"component": "TableCell",
"props": {
"text": "{{column.column_default || '-'}}"
}
}
]
}
]
}
]
}
]
}
]
}
]
}
},
"componentProps": {

View File

@@ -0,0 +1,270 @@
'use client';
import React from 'react';
import {
Box,
Button,
Typography,
Paper,
TextField,
Select,
MenuItem,
Checkbox,
FormControlLabel,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip,
Tooltip,
FormControl,
InputLabel,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import TableChartIcon from '@mui/icons-material/TableChart';
import SpeedIcon from '@mui/icons-material/Speed';
import { ComponentNode } from './featureConfig';
// Map of component names to actual components
const componentMap: Record<string, React.ComponentType<any>> = {
Box,
Button,
Typography,
Paper,
TextField,
Select,
MenuItem,
Checkbox,
FormControlLabel,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip,
Tooltip,
FormControl,
InputLabel,
};
// Map of icon names to icon components
const iconMap: Record<string, React.ComponentType<any>> = {
Add: AddIcon,
Delete: DeleteIcon,
Edit: EditIcon,
TableChart: TableChartIcon,
Speed: SpeedIcon,
};
type ComponentTreeRendererProps = {
tree: ComponentNode;
data?: Record<string, any>;
handlers?: Record<string, (...args: any[]) => void>;
};
/**
* Evaluate a condition string with the provided data context
*/
function evaluateCondition(condition: string, data: Record<string, any>): boolean {
try {
// Create a function that evaluates the condition in the data context
const func = new Function(...Object.keys(data), `return ${condition}`);
return func(...Object.values(data));
} catch (error) {
console.error('Error evaluating condition:', condition, error);
return false;
}
}
/**
* Interpolate template strings like {{variable}} with actual values from data
*/
function interpolateValue(value: any, data: Record<string, any>): any {
if (typeof value !== 'string') {
return value;
}
// Check if it's a template string
const templateMatch = value.match(/^\{\{(.+)\}\}$/);
if (templateMatch) {
const expression = templateMatch[1].trim();
try {
const func = new Function(...Object.keys(data), `return ${expression}`);
return func(...Object.values(data));
} catch (error) {
console.error('Error evaluating expression:', expression, error);
return value;
}
}
// Replace inline templates
return value.replace(/\{\{(.+?)\}\}/g, (_, expression) => {
try {
const func = new Function(...Object.keys(data), `return ${expression.trim()}`);
return func(...Object.values(data));
} catch (error) {
console.error('Error evaluating inline expression:', expression, error);
return '';
}
});
}
/**
* Interpolate all props in an object
*/
function interpolateProps(
props: Record<string, any> | undefined,
data: Record<string, any>,
handlers: Record<string, (...args: any[]) => void>
): Record<string, any> {
if (!props) return {};
const interpolated: Record<string, any> = {};
Object.entries(props).forEach(([key, value]) => {
if (typeof value === 'string' && handlers[value]) {
// If the value is a handler function name, use the handler
interpolated[key] = handlers[value];
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Recursively interpolate nested objects
interpolated[key] = interpolateProps(value, data, handlers);
} else {
// Interpolate the value
interpolated[key] = interpolateValue(value, data);
}
});
return interpolated;
}
/**
* Get the singular form of a plural word (simple implementation)
*/
function getSingular(plural: string): string {
if (plural.endsWith('ies')) {
return plural.slice(0, -3) + 'y';
}
if (plural.endsWith('es')) {
return plural.slice(0, -2);
}
if (plural.endsWith('s')) {
return plural.slice(0, -1);
}
return plural;
}
/**
* Render a single component node
*/
function renderNode(
node: ComponentNode,
data: Record<string, any>,
handlers: Record<string, (...args: any[]) => void>,
key: string | number
): React.ReactNode {
// Evaluate condition
if (node.condition && !evaluateCondition(node.condition, data)) {
return null;
}
// Handle forEach loops
if (node.forEach) {
const items = data[node.forEach];
if (!Array.isArray(items)) {
console.warn(`forEach data "${node.forEach}" is not an array`);
return null;
}
const singularName = getSingular(node.forEach);
return items.map((item, index) => {
const itemData = { ...data, [singularName]: item, index };
// Remove forEach from node to avoid infinite loop
const nodeWithoutForEach = { ...node, forEach: undefined };
return renderNode(nodeWithoutForEach, itemData, handlers, `${key}-${index}`);
});
}
// Get the component
const componentName = interpolateValue(node.component, data);
const Component = componentMap[componentName];
if (!Component) {
console.warn(`Component "${componentName}" not found in componentMap`);
return null;
}
// Interpolate props
const props = interpolateProps(node.props, data, handlers);
// Handle special props
if (props.startIcon && typeof props.startIcon === 'string') {
const IconComponent = iconMap[props.startIcon];
if (IconComponent) {
props.startIcon = <IconComponent />;
}
}
if (props.endIcon && typeof props.endIcon === 'string') {
const IconComponent = iconMap[props.endIcon];
if (IconComponent) {
props.endIcon = <IconComponent />;
}
}
// Handle 'text' prop for Typography and Button
let textContent = null;
if (props.text !== undefined) {
textContent = props.text;
delete props.text;
}
// Render children
const children = node.children?.map((child, index) =>
renderNode(child, data, handlers, `${key}-child-${index}`)
);
return (
<Component key={key} {...props}>
{textContent}
{children}
</Component>
);
}
/**
* ComponentTreeRenderer - Renders a component tree from JSON configuration
*/
export default function ComponentTreeRenderer({
tree,
data = {},
handlers = {},
}: ComponentTreeRendererProps) {
if (!tree) {
return null;
}
return <>{renderNode(tree, data, handlers, 'root')}</>;
}