Add component tree definitions - build complete UIs from JSON configuration

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 13:39:34 +00:00
parent be1b6f02d3
commit 7da561bd86
6 changed files with 2492 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
# Building Apps with features.json
**With a good enough features.json, you could build half the app with it!**
This example demonstrates how the enhanced configuration system enables declarative application building.
## Complete CRUD Interface Generator
```typescript
import {
getFormSchema,
getTableLayout,
getTableFeatures,
getColumnLayout,
getColumnFeatures,
getColumnTranslation,
getActionTranslation,
getApiEndpoints,
getPermissions,
getRelationships,
getUiViews,
hasPermission,
} from '@/utils/featureConfig';
/**
* Generates a complete CRUD interface from configuration
* This demonstrates how features.json can drive application generation
*/
export function generateCRUDInterface(
resourceName: string,
locale: 'en' | 'fr' = 'en',
userRole: string = 'user'
) {
// Get all configurations
const formSchema = getFormSchema(resourceName);
const tableLayout = getTableLayout(resourceName);
const tableFeatures = getTableFeatures(resourceName);
const apiEndpoints = getApiEndpoints(resourceName);
const permissions = getPermissions(resourceName);
const relationships = getRelationships(resourceName);
const uiViews = getUiViews(resourceName);
// Build column definitions
const columns = tableLayout?.columns.map(columnName => {
const layout = getColumnLayout(columnName);
const features = getColumnFeatures(columnName);
const label = getColumnTranslation(columnName, locale) || columnName;
return {
field: columnName,
label,
width: tableLayout.columnWidths[columnName],
align: layout?.align || 'left',
format: layout?.format || 'text',
editable: layout?.editable ?? true,
sortable: features?.sortable ?? true,
filterable: features?.filterable ?? true,
searchable: features?.searchable ?? true,
hidden: tableLayout.hiddenColumns?.includes(columnName) ?? false,
frozen: tableLayout.frozenColumns?.includes(columnName) ?? false,
};
});
// Build action buttons with permission checks
const actions = tableFeatures?.allowedActions
.filter(action => hasPermission(resourceName, action, userRole))
.map(action => ({
name: action,
label: getActionTranslation(action, locale),
endpoint: apiEndpoints?.[action],
permitted: true,
}));
// Build form configuration
const form = formSchema ? {
fields: formSchema.fields.map(field => ({
...field,
label: getColumnTranslation(field.name, locale) || field.label,
})),
submitLabel: formSchema.submitLabel,
cancelLabel: formSchema.cancelLabel,
} : null;
// Build complete interface configuration
return {
resource: resourceName,
locale,
userRole,
// List view
list: {
component: uiViews?.list?.component || 'DataGrid',
columns,
actions: actions?.filter(a => a.name === 'create'),
features: {
pagination: tableFeatures?.enablePagination ?? true,
search: tableFeatures?.enableSearch ?? true,
filters: tableFeatures?.enableFilters ?? true,
export: tableFeatures?.enableExport ?? false,
rowsPerPage: tableFeatures?.rowsPerPage || 25,
},
sorting: tableLayout?.defaultSort,
api: apiEndpoints?.list,
},
// Detail view
detail: {
component: uiViews?.detail?.component || 'DetailView',
columns,
actions: actions?.filter(a => ['update', 'delete'].includes(a.name)),
relationships: relationships,
tabs: uiViews?.detail?.tabs || ['info'],
api: apiEndpoints?.get,
},
// Create form
create: {
component: uiViews?.create?.component || 'FormDialog',
form,
api: apiEndpoints?.create,
redirect: uiViews?.create?.redirect || 'list',
enabled: hasPermission(resourceName, 'create', userRole),
},
// Edit form
edit: {
component: uiViews?.edit?.component || 'FormDialog',
form,
api: apiEndpoints?.update,
redirect: uiViews?.edit?.redirect || 'detail',
enabled: hasPermission(resourceName, 'update', userRole),
},
// Delete confirmation
delete: {
component: 'ConfirmDialog',
api: apiEndpoints?.delete,
enabled: hasPermission(resourceName, 'delete', userRole),
},
permissions,
relationships,
};
}
// Usage example
const usersInterface = generateCRUDInterface('users', 'en', 'admin');
console.log(usersInterface);
```
## Auto-Generated Form Component
```typescript
import { getFormSchema, getValidationRule } from '@/utils/featureConfig';
export function renderForm(resourceName: string) {
const schema = getFormSchema(resourceName);
if (!schema) return null;
return schema.fields.map(field => {
const validationRule = field.validation
? getValidationRule(field.validation)
: null;
return {
name: field.name,
type: field.type,
label: field.label,
placeholder: field.placeholder,
required: field.required,
validation: validationRule,
// Field-specific props
...(field.type === 'select' && { options: field.options }),
...(field.type === 'number' && {
min: field.min,
max: field.max,
step: field.step,
prefix: field.prefix,
suffix: field.suffix,
}),
...(field.type === 'text' && {
minLength: field.minLength,
maxLength: field.maxLength,
}),
...(field.type === 'textarea' && { rows: field.rows }),
...(field.type === 'checkbox' && { defaultValue: field.defaultValue }),
};
});
}
```
## Auto-Generated API Routes
```typescript
import { getApiEndpoint } from '@/utils/featureConfig';
export function makeApiCall(
resourceName: string,
action: string,
data?: any,
params?: Record<string, string>
) {
const endpoint = getApiEndpoint(resourceName, action);
if (!endpoint) {
throw new Error(`Endpoint not found: ${resourceName}.${action}`);
}
// Replace path parameters
let path = endpoint.path;
if (params) {
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, value);
});
}
// Make the API call
return fetch(path, {
method: endpoint.method,
headers: {
'Content-Type': 'application/json',
},
...(data && { body: JSON.stringify(data) }),
});
}
// Usage
await makeApiCall('users', 'list');
await makeApiCall('users', 'get', null, { id: '123' });
await makeApiCall('users', 'create', { name: 'John', email: 'john@example.com' });
await makeApiCall('users', 'update', { name: 'Jane' }, { id: '123' });
await makeApiCall('users', 'delete', null, { id: '123' });
```
## Permission-Based UI Rendering
```typescript
import { hasPermission, getPermissions } from '@/utils/featureConfig';
export function renderResourceActions(
resourceName: string,
userRole: string
) {
const permissions = getPermissions(resourceName);
const actions = [
{
name: 'create',
label: 'Create New',
icon: 'Add',
visible: hasPermission(resourceName, 'create', userRole),
},
{
name: 'update',
label: 'Edit',
icon: 'Edit',
visible: hasPermission(resourceName, 'update', userRole),
},
{
name: 'delete',
label: 'Delete',
icon: 'Delete',
visible: hasPermission(resourceName, 'delete', userRole),
},
];
return actions.filter(action => action.visible);
}
// Usage in React component
function UsersList({ userRole }: { userRole: string }) {
const actions = renderResourceActions('users', userRole);
return (
<div>
{actions.map(action => (
<Button key={action.name} startIcon={<Icon>{action.icon}</Icon>}>
{action.label}
</Button>
))}
</div>
);
}
```
## Relationship-Based Data Loading
```typescript
import { getRelationships, getApiEndpoint } from '@/utils/featureConfig';
export async function loadResourceWithRelations(
resourceName: string,
resourceId: string
) {
const relationships = getRelationships(resourceName);
const endpoint = getApiEndpoint(resourceName, 'get');
// Load main resource
const mainData = await fetch(
endpoint!.path.replace(':id', resourceId)
).then(r => r.json());
// Load related resources
const relatedData: Record<string, any> = {};
if (relationships?.hasMany) {
for (const relation of relationships.hasMany) {
const relationEndpoint = getApiEndpoint(relation, 'list');
if (relationEndpoint) {
relatedData[relation] = await fetch(
`${relationEndpoint.path}?${resourceName}_id=${resourceId}`
).then(r => r.json());
}
}
}
if (relationships?.belongsTo) {
for (const relation of relationships.belongsTo) {
const relationId = mainData[`${relation}_id`];
if (relationId) {
const relationEndpoint = getApiEndpoint(relation, 'get');
if (relationEndpoint) {
relatedData[relation] = await fetch(
relationEndpoint.path.replace(':id', relationId)
).then(r => r.json());
}
}
}
}
return {
...mainData,
_relations: relatedData,
};
}
// Usage
const userWithRelations = await loadResourceWithRelations('users', '123');
// Returns: { id: 123, name: 'John', _relations: { orders: [...], reviews: [...] } }
```
## Complete Page Generator
```typescript
import { generateCRUDInterface } from './crudGenerator';
/**
* Generates an entire CRUD page from configuration
* This is the ultimate example of configuration-driven development
*/
export function generateResourcePage(
resourceName: string,
locale: 'en' | 'fr',
userRole: string
) {
const config = generateCRUDInterface(resourceName, locale, userRole);
return {
// Page metadata
title: `${resourceName.charAt(0).toUpperCase() + resourceName.slice(1)} Management`,
breadcrumbs: ['Home', 'Admin', resourceName],
// Layout
layout: 'AdminLayout',
// Components to render
components: [
{
type: config.list.component,
props: {
columns: config.list.columns,
api: config.list.api,
features: config.list.features,
actions: config.list.actions,
sorting: config.list.sorting,
},
},
config.create.enabled && {
type: config.create.component,
props: {
fields: config.create.form?.fields,
submitLabel: config.create.form?.submitLabel,
cancelLabel: config.create.form?.cancelLabel,
api: config.create.api,
redirect: config.create.redirect,
},
},
config.edit.enabled && {
type: config.edit.component,
props: {
fields: config.edit.form?.fields,
api: config.edit.api,
redirect: config.edit.redirect,
},
},
config.delete.enabled && {
type: config.delete.component,
props: {
api: config.delete.api,
},
},
].filter(Boolean),
// Data loading
dataLoader: async () => {
const response = await fetch(config.list.api!.path);
return response.json();
},
// Permissions
requiredRole: userRole,
permissions: config.permissions,
};
}
// Generate entire pages from configuration
const usersPage = generateResourcePage('users', 'en', 'admin');
const productsPage = generateResourcePage('products', 'fr', 'editor');
```
## Benefits of Configuration-Driven Architecture
1. **Rapid Development**: Add new resources by just updating JSON
2. **Consistency**: All CRUD interfaces follow the same patterns
3. **Maintainability**: Changes to one config affect all resources
4. **Type Safety**: TypeScript types ensure config validity
5. **Testability**: Easy to test configuration vs. hardcoded logic
6. **Internationalization**: Built-in translation support
7. **Permission Management**: Centralized access control
8. **API Documentation**: Config serves as API documentation
9. **UI Generation**: Automatic form and table generation
10. **Flexibility**: Override defaults when needed
## What You Can Build from features.json
- ✅ Complete CRUD interfaces
- ✅ Forms with validation
- ✅ Data tables with sorting, filtering, pagination
- ✅ API routes and endpoints
- ✅ Permission-based UI
- ✅ Relationship loading
- ✅ Multi-language support
- ✅ Navigation menus
- ✅ Admin panels
- ✅ Resource management pages
**Truly, with a good features.json, you can build half the app!**

639
docs/COMPONENT_TREES.md Normal file
View File

@@ -0,0 +1,639 @@
# Component Trees in features.json
**Define entire UI hierarchies in JSON - build complete interfaces declaratively!**
The `componentTrees` section in features.json allows you to define complete component hierarchies in a declarative JSON format. This enables you to build entire pages and complex UIs without writing JSX code.
## Overview
Component trees support:
- ✅ Nested component hierarchies
- ✅ Props passing with interpolation
- ✅ Conditional rendering
- ✅ Loops/iterations with `forEach`
- ✅ Data binding with `dataSource`
- ✅ Event handlers
- ✅ Dynamic values with template syntax `{{variable}}`
## Basic Structure
```json
{
"componentTrees": {
"MyPage": {
"component": "Box",
"props": {
"sx": { "p": 3 }
},
"children": [
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "Hello World"
}
}
]
}
}
}
```
## Component Node Schema
```typescript
{
"component": string, // Component name (e.g., "Box", "Button", "DataGrid")
"props"?: object, // Component props
"children"?: ComponentNode[], // Child components
"condition"?: string, // Render condition (e.g., "hasPermission('create')")
"forEach"?: string, // Loop over data (e.g., "items", "users")
"dataSource"?: string, // Bind to data source (e.g., "tableData", "navItems")
"comment"?: string // Documentation comment
}
```
## Template Syntax
Use `{{variable}}` for dynamic values:
```json
{
"component": "Typography",
"props": {
"text": "Welcome, {{user.name}}!"
}
}
```
### Accessing Nested Properties
```json
{
"component": "Typography",
"props": {
"text": "{{user.profile.firstName}} {{user.profile.lastName}}"
}
}
```
### Using Expressions
```json
{
"component": "Icon",
"props": {
"name": "{{card.change > 0 ? 'TrendingUp' : 'TrendingDown'}}"
}
}
```
## Conditional Rendering
Use the `condition` property to conditionally render components:
```json
{
"component": "Button",
"condition": "hasPermission('create')",
"props": {
"text": "Create New",
"onClick": "openCreateDialog"
}
}
```
### Multiple Conditions
```json
{
"condition": "features.enableSearch && userRole === 'admin'",
"component": "TextField",
"props": {
"placeholder": "Search..."
}
}
```
## Loops with forEach
Iterate over arrays using `forEach`:
```json
{
"component": "Grid",
"forEach": "users",
"props": {
"item": true,
"xs": 12,
"sm": 6
},
"children": [
{
"component": "Card",
"children": [
{
"component": "Typography",
"props": {
"text": "{{user.name}}"
}
}
]
}
]
}
```
In the loop, the current item is available as the singular form of the array name:
- `forEach: "users"` → current item is `{{user}}`
- `forEach: "products"` → current item is `{{product}}`
- `forEach: "items"` → current item is `{{item}}`
## Data Sources
Bind components to data sources:
```json
{
"component": "NavList",
"dataSource": "navItems",
"children": [
{
"component": "NavItem",
"props": {
"icon": "{{item.icon}}",
"label": "{{item.label}}",
"href": "/admin/{{item.id}}"
}
}
]
}
```
## Event Handlers
Reference event handler functions by name:
```json
{
"component": "Button",
"props": {
"text": "Save",
"onClick": "handleSave"
}
}
```
Multiple handlers:
```json
{
"component": "TextField",
"props": {
"value": "{{searchTerm}}",
"onChange": "handleSearch",
"onKeyPress": "handleKeyPress"
}
}
```
## Complete Examples
### Admin Dashboard Layout
```json
{
"AdminDashboard": {
"component": "Box",
"props": {
"sx": { "display": "flex", "minHeight": "100vh" }
},
"children": [
{
"component": "Sidebar",
"props": { "width": 240 },
"children": [
{
"component": "NavList",
"dataSource": "navItems",
"children": [
{
"component": "NavItem",
"props": {
"icon": "{{item.icon}}",
"label": "{{item.label}}",
"href": "/admin/{{item.id}}"
}
}
]
}
]
},
{
"component": "Box",
"props": { "sx": { "flexGrow": 1 } },
"children": [
{
"component": "AppBar",
"children": [
{
"component": "Toolbar",
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"text": "{{pageTitle}}"
}
}
]
}
]
},
{
"component": "Outlet",
"comment": "Child routes render here"
}
]
}
]
}
}
```
### Resource List Page with CRUD Actions
```json
{
"ResourceListPage": {
"component": "Box",
"children": [
{
"component": "Box",
"props": {
"sx": { "display": "flex", "justifyContent": "space-between", "mb": 3 }
},
"children": [
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{resourceName}}"
}
},
{
"component": "Button",
"condition": "hasPermission('create')",
"props": {
"variant": "contained",
"startIcon": "Add",
"text": "Create New",
"onClick": "openCreateDialog"
}
}
]
},
{
"component": "DataGrid",
"dataSource": "tableData",
"props": {
"columns": "{{columns}}",
"rows": "{{rows}}",
"onEdit": "handleEdit",
"onDelete": "handleDelete"
}
},
{
"component": "Pagination",
"condition": "features.enablePagination",
"props": {
"count": "{{totalPages}}",
"page": "{{currentPage}}",
"onChange": "handlePageChange"
}
}
]
}
}
```
### Form Dialog
```json
{
"FormDialogTree": {
"component": "Dialog",
"props": {
"open": "{{open}}",
"onClose": "handleClose",
"maxWidth": "md"
},
"children": [
{
"component": "DialogTitle",
"children": [
{
"component": "Typography",
"props": {
"text": "{{title}}"
}
}
]
},
{
"component": "DialogContent",
"children": [
{
"component": "Grid",
"props": { "container": true, "spacing": 2 },
"children": [
{
"component": "Grid",
"forEach": "formFields",
"props": {
"item": true,
"xs": 12,
"sm": 6
},
"children": [
{
"component": "DynamicField",
"props": {
"field": "{{field}}",
"value": "{{values[field.name]}}",
"onChange": "handleFieldChange"
}
}
]
}
]
}
]
},
{
"component": "DialogActions",
"children": [
{
"component": "Button",
"props": {
"text": "Cancel",
"onClick": "handleClose"
}
},
{
"component": "Button",
"props": {
"variant": "contained",
"text": "Save",
"onClick": "handleSubmit",
"disabled": "{{!isValid}}"
}
}
]
}
]
}
}
```
### Dashboard Stats Cards
```json
{
"DashboardStatsCards": {
"component": "Grid",
"props": { "container": true, "spacing": 3 },
"children": [
{
"component": "Grid",
"forEach": "statsCards",
"props": {
"item": true,
"xs": 12,
"sm": 6,
"md": 3
},
"children": [
{
"component": "Card",
"children": [
{
"component": "CardContent",
"children": [
{
"component": "Icon",
"props": {
"name": "{{card.icon}}",
"color": "{{card.color}}"
}
},
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{card.value}}"
}
},
{
"component": "Typography",
"props": {
"variant": "body2",
"text": "{{card.label}}"
}
}
]
}
]
}
]
}
]
}
}
```
## Using Component Trees in Code
### Get a Component Tree
```typescript
import { getComponentTree } from '@/utils/featureConfig';
const tree = getComponentTree('AdminDashboard');
```
### Render a Component Tree
```typescript
import { getComponentTree } from '@/utils/featureConfig';
function ComponentTreeRenderer({ treeName, data, handlers }: Props) {
const tree = getComponentTree(treeName);
if (!tree) return null;
return renderNode(tree, data, handlers);
}
function renderNode(node: ComponentNode, data: any, handlers: any): JSX.Element {
const Component = getComponent(node.component);
// Evaluate condition
if (node.condition && !evaluateCondition(node.condition, data)) {
return null;
}
// Handle forEach loops
if (node.forEach) {
const items = data[node.forEach] || [];
return (
<>
{items.map((item: any, index: number) => {
const itemData = { ...data, [getSingular(node.forEach)]: item };
return renderNode({ ...node, forEach: undefined }, itemData, handlers);
})}
</>
);
}
// Interpolate props
const props = interpolateProps(node.props, data, handlers);
// Render children
const children = node.children?.map((child, idx) =>
renderNode(child, data, handlers)
);
return <Component key={index} {...props}>{children}</Component>;
}
```
### Complete Example with React
```typescript
import React from 'react';
import { getComponentTree } from '@/utils/featureConfig';
import { Box, Button, Typography, Dialog, TextField } from '@mui/material';
const componentMap = {
Box, Button, Typography, Dialog, TextField,
// ... other components
};
function DynamicPage({ treeName }: { treeName: string }) {
const tree = getComponentTree(treeName);
const [data, setData] = useState({
pageTitle: 'Users Management',
resourceName: 'Users',
rows: [],
loading: false,
});
const handlers = {
handleEdit: (row: any) => console.log('Edit', row),
handleDelete: (row: any) => console.log('Delete', row),
openCreateDialog: () => console.log('Create'),
};
return renderComponentTree(tree, data, handlers);
}
```
## Benefits of Component Trees
1. **Declarative UI**: Define UIs in configuration, not code
2. **Rapid Prototyping**: Build pages quickly without JSX
3. **Non-Technical Edits**: Allow non-developers to modify UI structure
4. **Consistency**: Enforce consistent component usage
5. **Dynamic Generation**: Generate UIs from API responses
6. **A/B Testing**: Easily swap component trees
7. **Version Control**: Track UI changes in JSON
8. **Hot Reloading**: Update UIs without code changes
9. **Multi-Platform**: Same tree can target web, mobile, etc.
10. **Reduced Code**: Less boilerplate, more configuration
## Best Practices
1. **Keep trees shallow**: Deep nesting is hard to maintain
2. **Use meaningful names**: `UserListPage` not `Page1`
3. **Document with comments**: Add `comment` fields for clarity
4. **Group related trees**: Organize by feature or page
5. **Validate props**: Ensure required props are present
6. **Test conditions**: Verify conditional logic works
7. **Handle missing data**: Provide fallbacks for `{{variables}}`
8. **Reuse subtrees**: Extract common patterns
9. **Type checking**: Use TypeScript for component props
10. **Version trees**: Track changes in version control
## Advanced Features
### Computed Values
```json
{
"component": "Typography",
"props": {
"text": "{{items.length}} items found"
}
}
```
### Nested Conditionals
```json
{
"condition": "user.role === 'admin'",
"component": "Box",
"children": [
{
"condition": "user.permissions.includes('delete')",
"component": "Button",
"props": {
"text": "Delete All",
"onClick": "handleDeleteAll"
}
}
]
}
```
### Dynamic Component Selection
```json
{
"component": "{{viewType === 'grid' ? 'GridView' : 'ListView'}}",
"props": {
"items": "{{items}}"
}
}
```
## API Reference
### `getComponentTree(treeName: string): ComponentTree | undefined`
Get a component tree by name.
```typescript
const tree = getComponentTree('AdminDashboard');
```
### `getAllComponentTrees(): Record<string, ComponentTree>`
Get all defined component trees.
```typescript
const trees = getAllComponentTrees();
console.log(Object.keys(trees)); // ['AdminDashboard', 'ResourceListPage', ...]
```
## Conclusion
Component trees in features.json enable you to:
- Build complete UIs without writing JSX
- Define page layouts declaratively
- Create dynamic, data-driven interfaces
- Rapidly prototype and iterate
- **Build half your app from configuration!**
With component trees, features.json becomes a complete UI definition language, enabling true configuration-driven development.

View File

@@ -0,0 +1,101 @@
# Features Configuration Guide
This guide explains how to use the enhanced `features.json` configuration system.
**With a good enough features.json, you could build half the app with it!**
The system now supports comprehensive declarative configuration for:
-**Translations** (i18n) for features, actions, tables, and columns
-**Action Namespaces** - Mapping UI actions to function names
-**Table Layouts** - Column ordering, widths, sorting, and visibility
-**Column Layouts** - Alignment, formatting, and editability
-**Table Features** - Pagination, search, export, and filters
-**Column Features** - Searchability, sortability, and validation
-**Component Layouts** - UI component display settings
## Quick Start
```typescript
import {
getFeatureTranslation,
getActionFunctionName,
getTableLayout,
getTableFeatures,
getComponentLayout
} from '@/utils/featureConfig';
// Get translated feature name
const feature = getFeatureTranslation('database-crud', 'en');
// { name: "Database CRUD Operations", description: "..." }
// Get action function name
const handler = getActionFunctionName('database-crud', 'create');
// "createRecord"
// Get table configuration
const layout = getTableLayout('users');
// { columns: [...], columnWidths: {...}, defaultSort: {...} }
```
## Complete API Reference
See the full configuration API at the end of this document.
## Building an App from Configuration
The enhanced features.json enables you to build complex UIs declaratively:
```typescript
// Example: Auto-generate a complete CRUD interface
function generateCRUDInterface(tableName: string, locale = 'en') {
const layout = getTableLayout(tableName);
const features = getTableFeatures(tableName);
const tableTranslation = getTableTranslation(tableName, locale);
return {
title: tableTranslation?.name,
columns: layout?.columns.map(col => ({
field: col,
label: getColumnTranslation(col, locale),
...getColumnLayout(col),
...getColumnFeatures(col)
})),
actions: features?.allowedActions.map(action => ({
name: action,
label: getActionTranslation(action, locale),
handler: getActionFunctionName('database-crud', action)
})),
settings: features
};
}
```
## API Functions
### Translations
- `getTranslations(locale?)` - Get all translations
- `getFeatureTranslation(featureId, locale?)` - Feature name/description
- `getActionTranslation(actionName, locale?)` - Action label
- `getTableTranslation(tableName, locale?)` - Table name/description
- `getColumnTranslation(columnName, locale?)` - Column label
### Actions
- `getActionFunctionName(featureId, actionName)` - Get handler function name
### Layouts
- `getTableLayout(tableName)` - Table display config
- `getColumnLayout(columnName)` - Column display config
- `getComponentLayout(componentName)` - Component config
### Features
- `getTableFeatures(tableName)` - Table capabilities
- `getColumnFeatures(columnName)` - Column capabilities
- `getFeatures()` - All enabled features
- `getFeatureById(id)` - Specific feature
- `getNavItems()` - Navigation items
### Other
- `getDataTypes()` - Database data types
- `getConstraintTypes()` - Constraint types
- `getQueryOperators()` - Query operators
- `getIndexTypes()` - Index types

View File

@@ -325,6 +325,816 @@
"elevation": 2
}
},
"formSchemas": {
"users": {
"fields": [
{
"name": "name",
"type": "text",
"label": "Name",
"placeholder": "Enter full name",
"required": true,
"minLength": 2,
"maxLength": 100
},
{
"name": "email",
"type": "email",
"label": "Email",
"placeholder": "user@example.com",
"required": true,
"validation": "email"
},
{
"name": "role",
"type": "select",
"label": "Role",
"required": true,
"options": [
{ "value": "admin", "label": "Administrator" },
{ "value": "user", "label": "User" },
{ "value": "guest", "label": "Guest" }
]
},
{
"name": "active",
"type": "checkbox",
"label": "Active",
"defaultValue": true
}
],
"submitLabel": "Save User",
"cancelLabel": "Cancel"
},
"products": {
"fields": [
{
"name": "name",
"type": "text",
"label": "Product Name",
"placeholder": "Enter product name",
"required": true,
"minLength": 3,
"maxLength": 200
},
{
"name": "description",
"type": "textarea",
"label": "Description",
"placeholder": "Product description",
"rows": 4,
"maxLength": 1000
},
{
"name": "price",
"type": "number",
"label": "Price",
"placeholder": "0.00",
"required": true,
"min": 0,
"step": 0.01,
"prefix": "$"
},
{
"name": "stock",
"type": "number",
"label": "Stock Quantity",
"placeholder": "0",
"required": true,
"min": 0,
"step": 1
},
{
"name": "category",
"type": "select",
"label": "Category",
"required": true,
"options": [
{ "value": "electronics", "label": "Electronics" },
{ "value": "clothing", "label": "Clothing" },
{ "value": "food", "label": "Food" },
{ "value": "books", "label": "Books" }
]
},
{
"name": "available",
"type": "checkbox",
"label": "Available for Purchase",
"defaultValue": true
}
],
"submitLabel": "Save Product",
"cancelLabel": "Cancel"
}
},
"validationRules": {
"email": {
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"message": "Please enter a valid email address"
},
"phone": {
"pattern": "^[+]?[(]?[0-9]{1,4}[)]?[-\\s\\.]?[(]?[0-9]{1,4}[)]?[-\\s\\.]?[0-9]{1,9}$",
"message": "Please enter a valid phone number"
},
"url": {
"pattern": "^(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$",
"message": "Please enter a valid URL"
},
"number": {
"pattern": "^-?\\d*\\.?\\d+$",
"message": "Please enter a valid number"
},
"integer": {
"pattern": "^-?\\d+$",
"message": "Please enter a valid integer"
},
"alphanumeric": {
"pattern": "^[a-zA-Z0-9]+$",
"message": "Only letters and numbers are allowed"
},
"username": {
"pattern": "^[a-zA-Z0-9_-]{3,20}$",
"message": "Username must be 3-20 characters and contain only letters, numbers, underscores, or hyphens"
}
},
"apiEndpoints": {
"users": {
"list": {
"method": "GET",
"path": "/api/admin/users",
"description": "List all users with pagination"
},
"get": {
"method": "GET",
"path": "/api/admin/users/:id",
"description": "Get a single user by ID"
},
"create": {
"method": "POST",
"path": "/api/admin/users",
"description": "Create a new user"
},
"update": {
"method": "PUT",
"path": "/api/admin/users/:id",
"description": "Update an existing user"
},
"delete": {
"method": "DELETE",
"path": "/api/admin/users/:id",
"description": "Delete a user"
}
},
"products": {
"list": {
"method": "GET",
"path": "/api/admin/products",
"description": "List all products with pagination"
},
"get": {
"method": "GET",
"path": "/api/admin/products/:id",
"description": "Get a single product by ID"
},
"create": {
"method": "POST",
"path": "/api/admin/products",
"description": "Create a new product"
},
"update": {
"method": "PUT",
"path": "/api/admin/products/:id",
"description": "Update an existing product"
},
"delete": {
"method": "DELETE",
"path": "/api/admin/products/:id",
"description": "Delete a product"
}
}
},
"permissions": {
"users": {
"create": ["admin"],
"read": ["admin", "user"],
"update": ["admin"],
"delete": ["admin"]
},
"products": {
"create": ["admin", "editor"],
"read": ["admin", "editor", "user"],
"update": ["admin", "editor"],
"delete": ["admin"]
}
},
"relationships": {
"users": {
"hasMany": ["orders", "reviews"],
"belongsTo": []
},
"products": {
"hasMany": ["reviews", "orderItems"],
"belongsTo": ["category"]
},
"orders": {
"belongsTo": ["users"],
"hasMany": ["orderItems"]
}
},
"uiViews": {
"users": {
"list": {
"component": "DataGrid",
"showActions": true,
"showSearch": true,
"showFilters": true,
"showExport": true
},
"detail": {
"component": "DetailView",
"showRelated": true,
"tabs": ["info", "orders", "activity"]
},
"create": {
"component": "FormDialog",
"redirect": "list"
},
"edit": {
"component": "FormDialog",
"redirect": "detail"
}
},
"products": {
"list": {
"component": "DataGrid",
"showActions": true,
"showSearch": true,
"showFilters": true,
"showExport": true
},
"detail": {
"component": "DetailView",
"showRelated": true,
"tabs": ["info", "reviews", "inventory"]
},
"create": {
"component": "FormDialog",
"redirect": "list"
},
"edit": {
"component": "FormDialog",
"redirect": "detail"
}
}
},
"componentTrees": {
"AdminDashboard": {
"component": "Box",
"props": {
"sx": { "display": "flex", "minHeight": "100vh" }
},
"children": [
{
"component": "Sidebar",
"props": {
"width": 240,
"variant": "permanent"
},
"children": [
{
"component": "Box",
"props": { "sx": { "p": 2 } },
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"text": "Admin Panel"
}
}
]
},
{
"component": "NavList",
"dataSource": "navItems",
"children": [
{
"component": "NavItem",
"props": {
"icon": "{{item.icon}}",
"label": "{{item.label}}",
"href": "/admin/{{item.id}}"
}
}
]
}
]
},
{
"component": "Box",
"props": {
"sx": { "flexGrow": 1, "display": "flex", "flexDirection": "column" }
},
"children": [
{
"component": "AppBar",
"props": {
"position": "sticky",
"elevation": 1
},
"children": [
{
"component": "Toolbar",
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"text": "{{pageTitle}}"
}
},
{
"component": "Box",
"props": { "sx": { "flexGrow": 1 } }
},
{
"component": "IconButton",
"props": {
"icon": "AccountCircle",
"onClick": "openUserMenu"
}
}
]
}
]
},
{
"component": "Box",
"props": {
"sx": { "p": 3, "flexGrow": 1 }
},
"children": [
{
"component": "Outlet",
"comment": "Child routes render here"
}
]
}
]
}
]
},
"ResourceListPage": {
"component": "Box",
"children": [
{
"component": "Box",
"props": {
"sx": { "display": "flex", "justifyContent": "space-between", "mb": 3 }
},
"children": [
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{resourceName}}"
}
},
{
"component": "Box",
"props": { "sx": { "display": "flex", "gap": 2 } },
"children": [
{
"component": "Button",
"condition": "hasPermission('create')",
"props": {
"variant": "contained",
"startIcon": "Add",
"text": "Create New",
"onClick": "openCreateDialog"
}
},
{
"component": "Button",
"condition": "features.enableExport",
"props": {
"variant": "outlined",
"startIcon": "Download",
"text": "Export",
"onClick": "handleExport"
}
}
]
}
]
},
{
"component": "Paper",
"props": { "sx": { "mb": 2 } },
"condition": "features.enableSearch || features.enableFilters",
"children": [
{
"component": "Box",
"props": { "sx": { "p": 2, "display": "flex", "gap": 2 } },
"children": [
{
"component": "TextField",
"condition": "features.enableSearch",
"props": {
"placeholder": "Search...",
"variant": "outlined",
"size": "small",
"fullWidth": true,
"onChange": "handleSearch"
}
},
{
"component": "Button",
"condition": "features.enableFilters",
"props": {
"variant": "outlined",
"startIcon": "FilterList",
"text": "Filters",
"onClick": "toggleFilters"
}
}
]
}
]
},
{
"component": "DataGrid",
"dataSource": "tableData",
"props": {
"columns": "{{columns}}",
"rows": "{{rows}}",
"loading": "{{loading}}",
"onEdit": "handleEdit",
"onDelete": "handleDelete",
"primaryKey": "id"
}
},
{
"component": "Box",
"props": { "sx": { "mt": 2, "display": "flex", "justifyContent": "center" } },
"condition": "features.enablePagination",
"children": [
{
"component": "Pagination",
"props": {
"count": "{{totalPages}}",
"page": "{{currentPage}}",
"onChange": "handlePageChange"
}
}
]
}
]
},
"ResourceDetailPage": {
"component": "Box",
"children": [
{
"component": "Box",
"props": { "sx": { "display": "flex", "justifyContent": "space-between", "mb": 3 } },
"children": [
{
"component": "Box",
"children": [
{
"component": "Button",
"props": {
"startIcon": "ArrowBack",
"text": "Back to List",
"onClick": "goBack"
}
},
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{resourceName}} #{{id}}",
"sx": { "mt": 2 }
}
}
]
},
{
"component": "Box",
"props": { "sx": { "display": "flex", "gap": 2 } },
"children": [
{
"component": "Button",
"condition": "hasPermission('update')",
"props": {
"variant": "contained",
"startIcon": "Edit",
"text": "Edit",
"onClick": "openEditDialog"
}
},
{
"component": "Button",
"condition": "hasPermission('delete')",
"props": {
"variant": "outlined",
"color": "error",
"startIcon": "Delete",
"text": "Delete",
"onClick": "openDeleteDialog"
}
}
]
}
]
},
{
"component": "Tabs",
"props": {
"value": "{{activeTab}}",
"onChange": "handleTabChange"
},
"children": [
{
"component": "Tab",
"forEach": "tabs",
"props": {
"label": "{{tab.label}}",
"value": "{{tab.value}}"
}
}
]
},
{
"component": "TabPanel",
"forEach": "tabs",
"props": {
"value": "{{tab.value}}",
"activeTab": "{{activeTab}}"
},
"children": [
{
"component": "Paper",
"props": { "sx": { "p": 3 } },
"children": [
{
"component": "Grid",
"props": { "container": true, "spacing": 2 },
"children": [
{
"component": "Grid",
"forEach": "columns",
"props": {
"item": true,
"xs": 12,
"sm": 6,
"md": 4
},
"children": [
{
"component": "Typography",
"props": {
"variant": "caption",
"color": "text.secondary",
"text": "{{column.label}}"
}
},
{
"component": "Typography",
"props": {
"variant": "body1",
"text": "{{data[column.name]}}"
}
}
]
}
]
}
]
}
]
},
{
"component": "Box",
"condition": "relationships && relationships.hasMany.length > 0",
"props": { "sx": { "mt": 4 } },
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"text": "Related Records",
"sx": { "mb": 2 }
}
},
{
"component": "Accordion",
"forEach": "relationships.hasMany",
"children": [
{
"component": "AccordionSummary",
"props": {
"expandIcon": "ExpandMore"
},
"children": [
{
"component": "Typography",
"props": {
"text": "{{relation.name}} ({{relation.count}})"
}
}
]
},
{
"component": "AccordionDetails",
"children": [
{
"component": "DataGrid",
"props": {
"columns": "{{relation.columns}}",
"rows": "{{relation.data}}",
"size": "small"
}
}
]
}
]
}
]
}
]
},
"FormDialogTree": {
"component": "Dialog",
"props": {
"open": "{{open}}",
"onClose": "handleClose",
"maxWidth": "md",
"fullWidth": true
},
"children": [
{
"component": "DialogTitle",
"children": [
{
"component": "Typography",
"props": {
"variant": "h6",
"text": "{{title}}"
}
}
]
},
{
"component": "DialogContent",
"children": [
{
"component": "Box",
"props": { "sx": { "pt": 2 } },
"children": [
{
"component": "Grid",
"props": { "container": true, "spacing": 2 },
"children": [
{
"component": "Grid",
"forEach": "formFields",
"props": {
"item": true,
"xs": 12,
"sm": "{{field.fullWidth ? 12 : 6}}"
},
"children": [
{
"component": "DynamicField",
"props": {
"field": "{{field}}",
"value": "{{values[field.name]}}",
"error": "{{errors[field.name]}}",
"onChange": "handleFieldChange"
}
}
]
}
]
}
]
}
]
},
{
"component": "DialogActions",
"children": [
{
"component": "Button",
"props": {
"text": "{{cancelLabel}}",
"onClick": "handleClose"
}
},
{
"component": "Button",
"props": {
"variant": "contained",
"text": "{{submitLabel}}",
"onClick": "handleSubmit",
"disabled": "{{loading || !isValid}}"
}
}
]
}
]
},
"DashboardStatsCards": {
"component": "Grid",
"props": { "container": true, "spacing": 3 },
"children": [
{
"component": "Grid",
"forEach": "statsCards",
"props": {
"item": true,
"xs": 12,
"sm": 6,
"md": 3
},
"children": [
{
"component": "Card",
"children": [
{
"component": "CardContent",
"children": [
{
"component": "Box",
"props": {
"sx": { "display": "flex", "alignItems": "center", "mb": 2 }
},
"children": [
{
"component": "Icon",
"props": {
"name": "{{card.icon}}",
"color": "{{card.color}}",
"sx": { "fontSize": 40, "mr": 2 }
}
},
{
"component": "Box",
"children": [
{
"component": "Typography",
"props": {
"variant": "h4",
"text": "{{card.value}}"
}
},
{
"component": "Typography",
"props": {
"variant": "body2",
"color": "text.secondary",
"text": "{{card.label}}"
}
}
]
}
]
},
{
"component": "Box",
"condition": "card.change",
"props": {
"sx": { "display": "flex", "alignItems": "center" }
},
"children": [
{
"component": "Icon",
"props": {
"name": "{{card.change > 0 ? 'TrendingUp' : 'TrendingDown'}}",
"color": "{{card.change > 0 ? 'success' : 'error'}}",
"fontSize": "small"
}
},
{
"component": "Typography",
"props": {
"variant": "caption",
"color": "{{card.change > 0 ? 'success.main' : 'error.main'}}",
"text": "{{Math.abs(card.change)}}%"
}
}
]
}
]
}
]
}
]
}
]
}
},
"features": [
{
"id": "database-crud",

View File

@@ -17,6 +17,17 @@ import {
getTableFeatures,
getColumnFeatures,
getComponentLayout,
getFormSchema,
getValidationRule,
getApiEndpoints,
getApiEndpoint,
getPermissions,
hasPermission,
getRelationships,
getUiViews,
getUiView,
getComponentTree,
getAllComponentTrees,
} from './featureConfig';
describe('FeatureConfig', () => {
@@ -678,4 +689,357 @@ describe('FeatureConfig', () => {
expect(layout).toBeUndefined();
});
});
describe('getFormSchema', () => {
it('should return form schema for users table', () => {
const schema = getFormSchema('users');
expect(schema).toBeDefined();
expect(schema?.fields).toBeDefined();
expect(Array.isArray(schema?.fields)).toBe(true);
expect(schema?.submitLabel).toBe('Save User');
expect(schema?.cancelLabel).toBe('Cancel');
});
it('should have name field in users schema', () => {
const schema = getFormSchema('users');
const nameField = schema?.fields.find(f => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField?.type).toBe('text');
expect(nameField?.required).toBe(true);
});
it('should return form schema for products table', () => {
const schema = getFormSchema('products');
expect(schema).toBeDefined();
expect(schema?.fields).toBeDefined();
expect(schema?.submitLabel).toBe('Save Product');
});
it('should have price field with number type in products schema', () => {
const schema = getFormSchema('products');
const priceField = schema?.fields.find(f => f.name === 'price');
expect(priceField).toBeDefined();
expect(priceField?.type).toBe('number');
expect(priceField?.required).toBe(true);
expect(priceField?.prefix).toBe('$');
});
});
describe('getValidationRule', () => {
it('should return validation rule for email', () => {
const rule = getValidationRule('email');
expect(rule).toBeDefined();
expect(rule?.pattern).toBeDefined();
expect(rule?.message).toContain('email');
});
it('should return validation rule for phone', () => {
const rule = getValidationRule('phone');
expect(rule).toBeDefined();
expect(rule?.pattern).toBeDefined();
expect(rule?.message).toContain('phone');
});
it('should return validation rule for number', () => {
const rule = getValidationRule('number');
expect(rule).toBeDefined();
expect(rule?.message).toContain('number');
});
});
describe('getApiEndpoints', () => {
it('should return all endpoints for users resource', () => {
const endpoints = getApiEndpoints('users');
expect(endpoints).toBeDefined();
expect(endpoints?.list).toBeDefined();
expect(endpoints?.get).toBeDefined();
expect(endpoints?.create).toBeDefined();
expect(endpoints?.update).toBeDefined();
expect(endpoints?.delete).toBeDefined();
});
it('should return all endpoints for products resource', () => {
const endpoints = getApiEndpoints('products');
expect(endpoints).toBeDefined();
expect(endpoints?.list).toBeDefined();
});
});
describe('getApiEndpoint', () => {
it('should return list endpoint for users', () => {
const endpoint = getApiEndpoint('users', 'list');
expect(endpoint).toBeDefined();
expect(endpoint?.method).toBe('GET');
expect(endpoint?.path).toBe('/api/admin/users');
});
it('should return create endpoint for users', () => {
const endpoint = getApiEndpoint('users', 'create');
expect(endpoint).toBeDefined();
expect(endpoint?.method).toBe('POST');
expect(endpoint?.path).toBe('/api/admin/users');
});
it('should return update endpoint for products', () => {
const endpoint = getApiEndpoint('products', 'update');
expect(endpoint).toBeDefined();
expect(endpoint?.method).toBe('PUT');
expect(endpoint?.path).toBe('/api/admin/products/:id');
});
});
describe('getPermissions', () => {
it('should return permissions for users resource', () => {
const permissions = getPermissions('users');
expect(permissions).toBeDefined();
expect(permissions?.create).toContain('admin');
expect(permissions?.read).toContain('admin');
expect(permissions?.read).toContain('user');
});
it('should return permissions for products resource', () => {
const permissions = getPermissions('products');
expect(permissions).toBeDefined();
expect(permissions?.create).toContain('admin');
expect(permissions?.create).toContain('editor');
});
});
describe('hasPermission', () => {
it('should return true when user has permission', () => {
expect(hasPermission('users', 'create', 'admin')).toBe(true);
expect(hasPermission('users', 'read', 'user')).toBe(true);
});
it('should return false when user does not have permission', () => {
expect(hasPermission('users', 'create', 'user')).toBe(false);
expect(hasPermission('users', 'delete', 'guest')).toBe(false);
});
it('should check product permissions correctly', () => {
expect(hasPermission('products', 'create', 'editor')).toBe(true);
expect(hasPermission('products', 'update', 'editor')).toBe(true);
expect(hasPermission('products', 'delete', 'editor')).toBe(false);
});
});
describe('getRelationships', () => {
it('should return relationships for users table', () => {
const relationships = getRelationships('users');
expect(relationships).toBeDefined();
expect(relationships?.hasMany).toContain('orders');
expect(relationships?.hasMany).toContain('reviews');
});
it('should return relationships for products table', () => {
const relationships = getRelationships('products');
expect(relationships).toBeDefined();
expect(relationships?.hasMany).toContain('reviews');
expect(relationships?.belongsTo).toContain('category');
});
it('should return relationships for orders table', () => {
const relationships = getRelationships('orders');
expect(relationships).toBeDefined();
expect(relationships?.belongsTo).toContain('users');
expect(relationships?.hasMany).toContain('orderItems');
});
});
describe('getUiViews', () => {
it('should return all views for users resource', () => {
const views = getUiViews('users');
expect(views).toBeDefined();
expect(views?.list).toBeDefined();
expect(views?.detail).toBeDefined();
expect(views?.create).toBeDefined();
expect(views?.edit).toBeDefined();
});
it('should return all views for products resource', () => {
const views = getUiViews('products');
expect(views).toBeDefined();
expect(views?.list).toBeDefined();
});
});
describe('getUiView', () => {
it('should return list view configuration for users', () => {
const view = getUiView('users', 'list');
expect(view).toBeDefined();
expect(view?.component).toBe('DataGrid');
expect(view?.showActions).toBe(true);
expect(view?.showSearch).toBe(true);
expect(view?.showFilters).toBe(true);
});
it('should return detail view configuration for users', () => {
const view = getUiView('users', 'detail');
expect(view).toBeDefined();
expect(view?.component).toBe('DetailView');
expect(view?.showRelated).toBe(true);
expect(view?.tabs).toContain('info');
expect(view?.tabs).toContain('orders');
});
it('should return create view configuration with redirect', () => {
const view = getUiView('users', 'create');
expect(view).toBeDefined();
expect(view?.component).toBe('FormDialog');
expect(view?.redirect).toBe('list');
});
it('should return edit view configuration for products', () => {
const view = getUiView('products', 'edit');
expect(view).toBeDefined();
expect(view?.redirect).toBe('detail');
});
});
describe('getComponentTree', () => {
it('should return component tree for AdminDashboard', () => {
const tree = getComponentTree('AdminDashboard');
expect(tree).toBeDefined();
expect(tree?.component).toBe('Box');
expect(tree?.children).toBeDefined();
expect(Array.isArray(tree?.children)).toBe(true);
});
it('should have Sidebar in AdminDashboard tree', () => {
const tree = getComponentTree('AdminDashboard');
const sidebar = tree?.children?.find(child => child.component === 'Sidebar');
expect(sidebar).toBeDefined();
expect(sidebar?.props?.width).toBe(240);
});
it('should return component tree for ResourceListPage', () => {
const tree = getComponentTree('ResourceListPage');
expect(tree).toBeDefined();
expect(tree?.component).toBe('Box');
expect(tree?.children).toBeDefined();
});
it('should have DataGrid in ResourceListPage tree', () => {
const tree = getComponentTree('ResourceListPage');
function findComponent(node: any, componentName: string): any {
if (node.component === componentName) return node;
if (node.children) {
for (const child of node.children) {
const found = findComponent(child, componentName);
if (found) return found;
}
}
return null;
}
const dataGrid = findComponent(tree, 'DataGrid');
expect(dataGrid).toBeDefined();
expect(dataGrid?.dataSource).toBe('tableData');
});
it('should return component tree for FormDialogTree', () => {
const tree = getComponentTree('FormDialogTree');
expect(tree).toBeDefined();
expect(tree?.component).toBe('Dialog');
});
it('should have conditional rendering in component tree', () => {
const tree = getComponentTree('ResourceListPage');
function findNodeWithCondition(node: any): any {
if (node.condition) return node;
if (node.children) {
for (const child of node.children) {
const found = findNodeWithCondition(child);
if (found) return found;
}
}
return null;
}
const conditionalNode = findNodeWithCondition(tree);
expect(conditionalNode).toBeDefined();
expect(conditionalNode?.condition).toBeDefined();
});
it('should have forEach loops in component tree', () => {
const tree = getComponentTree('ResourceDetailPage');
function findNodeWithForEach(node: any): any {
if (node.forEach) return node;
if (node.children) {
for (const child of node.children) {
const found = findNodeWithForEach(child);
if (found) return found;
}
}
return null;
}
const loopNode = findNodeWithForEach(tree);
expect(loopNode).toBeDefined();
expect(loopNode?.forEach).toBeDefined();
});
});
describe('getAllComponentTrees', () => {
it('should return all component trees', () => {
const trees = getAllComponentTrees();
expect(trees).toBeDefined();
expect(typeof trees).toBe('object');
});
it('should include AdminDashboard tree', () => {
const trees = getAllComponentTrees();
expect(trees.AdminDashboard).toBeDefined();
});
it('should include ResourceListPage tree', () => {
const trees = getAllComponentTrees();
expect(trees.ResourceListPage).toBeDefined();
});
it('should include FormDialogTree tree', () => {
const trees = getAllComponentTrees();
expect(trees.FormDialogTree).toBeDefined();
});
it('should include DashboardStatsCards tree', () => {
const trees = getAllComponentTrees();
expect(trees.DashboardStatsCards).toBeDefined();
});
});
});

View File

@@ -109,6 +109,79 @@ export type ComponentLayout = {
[key: string]: any;
};
export type FormField = {
name: string;
type: 'text' | 'email' | 'number' | 'textarea' | 'select' | 'checkbox' | 'date' | 'datetime';
label: string;
placeholder?: string;
required?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
step?: number;
rows?: number;
defaultValue?: any;
options?: Array<{ value: string; label: string }>;
validation?: string;
prefix?: string;
suffix?: string;
};
export type FormSchema = {
fields: FormField[];
submitLabel: string;
cancelLabel: string;
};
export type ValidationRule = {
pattern: string;
message: string;
};
export type ApiEndpoint = {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
path: string;
description: string;
};
export type Permissions = {
create?: string[];
read?: string[];
update?: string[];
delete?: string[];
};
export type Relationships = {
hasMany?: string[];
belongsTo?: string[];
hasOne?: string[];
belongsToMany?: string[];
};
export type UiView = {
component: string;
showActions?: boolean;
showSearch?: boolean;
showFilters?: boolean;
showExport?: boolean;
showRelated?: boolean;
tabs?: string[];
redirect?: string;
};
export type ComponentNode = {
component: string;
props?: Record<string, any>;
children?: ComponentNode[];
condition?: string;
forEach?: string;
dataSource?: string;
comment?: string;
};
export type ComponentTree = ComponentNode;
// Type definition for the features config structure
type FeaturesConfig = {
translations?: Translations;
@@ -118,6 +191,13 @@ type FeaturesConfig = {
tableFeatures?: Record<string, TableFeatures>;
columnFeatures?: Record<string, ColumnFeatures>;
componentLayouts?: Record<string, ComponentLayout>;
formSchemas?: Record<string, FormSchema>;
validationRules?: Record<string, ValidationRule>;
apiEndpoints?: Record<string, Record<string, ApiEndpoint>>;
permissions?: Record<string, Permissions>;
relationships?: Record<string, Relationships>;
uiViews?: Record<string, Record<string, UiView>>;
componentTrees?: Record<string, ComponentTree>;
features: Feature[];
dataTypes: DataType[];
constraintTypes?: ConstraintType[];
@@ -208,3 +288,49 @@ export function getColumnFeatures(columnName: string): ColumnFeatures | undefine
export function getComponentLayout(componentName: string): ComponentLayout | undefined {
return config.componentLayouts?.[componentName];
}
export function getFormSchema(tableName: string): FormSchema | undefined {
return config.formSchemas?.[tableName];
}
export function getValidationRule(ruleName: string): ValidationRule | undefined {
return config.validationRules?.[ruleName];
}
export function getApiEndpoints(resourceName: string): Record<string, ApiEndpoint> | undefined {
return config.apiEndpoints?.[resourceName];
}
export function getApiEndpoint(resourceName: string, action: string): ApiEndpoint | undefined {
return config.apiEndpoints?.[resourceName]?.[action];
}
export function getPermissions(resourceName: string): Permissions | undefined {
return config.permissions?.[resourceName];
}
export function hasPermission(resourceName: string, action: string, userRole: string): boolean {
const permissions = config.permissions?.[resourceName];
const allowedRoles = permissions?.[action as keyof Permissions];
return allowedRoles?.includes(userRole) ?? false;
}
export function getRelationships(tableName: string): Relationships | undefined {
return config.relationships?.[tableName];
}
export function getUiViews(resourceName: string): Record<string, UiView> | undefined {
return config.uiViews?.[resourceName];
}
export function getUiView(resourceName: string, viewName: string): UiView | undefined {
return config.uiViews?.[resourceName]?.[viewName];
}
export function getComponentTree(treeName: string): ComponentTree | undefined {
return config.componentTrees?.[treeName];
}
export function getAllComponentTrees(): Record<string, ComponentTree> {
return config.componentTrees || {};
}