From 4233aadc3f9e2361cc409f02ff3994d26618a9ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:59:56 +0000 Subject: [PATCH] Add ComponentTreeRenderer and expand features.json with tab component trees Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/config/features.json | 341 ++++++++++++++++++++++++++++ src/utils/ComponentTreeRenderer.tsx | 270 ++++++++++++++++++++++ 2 files changed, 611 insertions(+) create mode 100644 src/utils/ComponentTreeRenderer.tsx diff --git a/src/config/features.json b/src/config/features.json index f9b6082..77fc987 100644 --- a/src/config/features.json +++ b/src/config/features.json @@ -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": { diff --git a/src/utils/ComponentTreeRenderer.tsx b/src/utils/ComponentTreeRenderer.tsx new file mode 100644 index 0000000..ef07d14 --- /dev/null +++ b/src/utils/ComponentTreeRenderer.tsx @@ -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> = { + 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> = { + Add: AddIcon, + Delete: DeleteIcon, + Edit: EditIcon, + TableChart: TableChartIcon, + Speed: SpeedIcon, +}; + +type ComponentTreeRendererProps = { + tree: ComponentNode; + data?: Record; + handlers?: Record void>; +}; + +/** + * Evaluate a condition string with the provided data context + */ +function evaluateCondition(condition: string, data: Record): 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): 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 | undefined, + data: Record, + handlers: Record void> +): Record { + if (!props) return {}; + + const interpolated: Record = {}; + + 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, + handlers: Record 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 = ; + } + } + + if (props.endIcon && typeof props.endIcon === 'string') { + const IconComponent = iconMap[props.endIcon]; + if (IconComponent) { + props.endIcon = ; + } + } + + // 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 ( + + {textContent} + {children} + + ); +} + +/** + * 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')}; +}