mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-25 06:15:02 +00:00
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:
452
docs/BUILDING_WITH_CONFIG.md
Normal file
452
docs/BUILDING_WITH_CONFIG.md
Normal 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
639
docs/COMPONENT_TREES.md
Normal 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.
|
||||
101
docs/FEATURES_CONFIG_GUIDE.md
Normal file
101
docs/FEATURES_CONFIG_GUIDE.md
Normal 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 || {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user