From 7da561bd86cd6633f2706c44f2f62dcd34c29644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:39:34 +0000 Subject: [PATCH] Add component tree definitions - build complete UIs from JSON configuration Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- docs/BUILDING_WITH_CONFIG.md | 452 ++++++++++++++++++ docs/COMPONENT_TREES.md | 639 +++++++++++++++++++++++++ docs/FEATURES_CONFIG_GUIDE.md | 101 ++++ src/config/features.json | 810 ++++++++++++++++++++++++++++++++ src/utils/featureConfig.test.ts | 364 ++++++++++++++ src/utils/featureConfig.ts | 126 +++++ 6 files changed, 2492 insertions(+) create mode 100644 docs/BUILDING_WITH_CONFIG.md create mode 100644 docs/COMPONENT_TREES.md create mode 100644 docs/FEATURES_CONFIG_GUIDE.md diff --git a/docs/BUILDING_WITH_CONFIG.md b/docs/BUILDING_WITH_CONFIG.md new file mode 100644 index 0000000..ecea97b --- /dev/null +++ b/docs/BUILDING_WITH_CONFIG.md @@ -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 +) { + 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 ( +
+ {actions.map(action => ( + + ))} +
+ ); +} +``` + +## 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 = {}; + + 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!** diff --git a/docs/COMPONENT_TREES.md b/docs/COMPONENT_TREES.md new file mode 100644 index 0000000..bcdf0eb --- /dev/null +++ b/docs/COMPONENT_TREES.md @@ -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 {children}; +} +``` + +### 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` + +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. diff --git a/docs/FEATURES_CONFIG_GUIDE.md b/docs/FEATURES_CONFIG_GUIDE.md new file mode 100644 index 0000000..2d66266 --- /dev/null +++ b/docs/FEATURES_CONFIG_GUIDE.md @@ -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 diff --git a/src/config/features.json b/src/config/features.json index e09d901..75b9256 100644 --- a/src/config/features.json +++ b/src/config/features.json @@ -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", diff --git a/src/utils/featureConfig.test.ts b/src/utils/featureConfig.test.ts index 41a1865..e27e93b 100644 --- a/src/utils/featureConfig.test.ts +++ b/src/utils/featureConfig.test.ts @@ -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(); + }); + }); }); diff --git a/src/utils/featureConfig.ts b/src/utils/featureConfig.ts index 9b2dea4..e6bf93a 100644 --- a/src/utils/featureConfig.ts +++ b/src/utils/featureConfig.ts @@ -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; + 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; columnFeatures?: Record; componentLayouts?: Record; + formSchemas?: Record; + validationRules?: Record; + apiEndpoints?: Record>; + permissions?: Record; + relationships?: Record; + uiViews?: Record>; + componentTrees?: Record; 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 | 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 | 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 { + return config.componentTrees || {}; +}