mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +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
|
||||
Reference in New Issue
Block a user