mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Generated by Spark: Add props configuration to pages.json for dynamic component props
This commit is contained in:
436
CONFIG_ARCHITECTURE.md
Normal file
436
CONFIG_ARCHITECTURE.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# Declarative Page Configuration System
|
||||
|
||||
## Overview
|
||||
|
||||
CodeForge uses a **declarative, JSON-driven configuration system** to manage pages, components, props, and layouts. This architecture enables rapid development without modifying core application logic.
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ App.tsx │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Uses page-loader to: │ │
|
||||
│ │ • Get enabled pages │ │
|
||||
│ │ • Resolve component props dynamically │ │
|
||||
│ │ • Render pages with correct configuration │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ imports & uses
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ page-loader.ts (Resolution Logic) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ • getPageConfig() │ │
|
||||
│ │ • getEnabledPages(featureToggles) │ │
|
||||
│ │ • resolveProps(propConfig, state, actions) │ │
|
||||
│ │ • getPageShortcuts() │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ reads
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ pages.json (Declarative Config) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ { │ │
|
||||
│ │ "pages": [ │ │
|
||||
│ │ { │ │
|
||||
│ │ "id": "models", │ │
|
||||
│ │ "component": "ModelDesigner", │ │
|
||||
│ │ "props": { │ │
|
||||
│ │ "state": ["models"], │ │
|
||||
│ │ "actions": ["onModelsChange:setModels"] │ │
|
||||
│ │ } │ │
|
||||
│ │ } │ │
|
||||
│ │ ] │ │
|
||||
│ │ } │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ validates
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ validate-config.ts (Validation Logic) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ • validatePageConfig() │ │
|
||||
│ │ • Checks for duplicate IDs/shortcuts │ │
|
||||
│ │ • Validates state/action keys exist │ │
|
||||
│ │ • Validates resizable configurations │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ uses schemas
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ page-schema.ts (Type Definitions) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ • PropConfigSchema │ │
|
||||
│ │ • ResizableConfigSchema │ │
|
||||
│ │ • SimplePageConfigSchema │ │
|
||||
│ │ • Zod schemas for runtime validation │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Core Files
|
||||
|
||||
### 1. `pages.json` - Source of Truth
|
||||
|
||||
The declarative configuration file that defines all pages, their components, and props.
|
||||
|
||||
**Location**: `/src/config/pages.json`
|
||||
|
||||
**Purpose**:
|
||||
- Define all application pages
|
||||
- Configure component props mapping
|
||||
- Set keyboard shortcuts
|
||||
- Configure resizable layouts
|
||||
- Enable/disable features
|
||||
|
||||
### 2. `page-loader.ts` - Resolution Engine
|
||||
|
||||
The TypeScript module that reads and processes the JSON configuration.
|
||||
|
||||
**Location**: `/src/config/page-loader.ts`
|
||||
|
||||
**Key Functions**:
|
||||
- `getPageConfig()` - Returns full page configuration
|
||||
- `getEnabledPages(featureToggles)` - Filters pages based on toggles
|
||||
- `resolveProps(propConfig, stateContext, actionContext)` - Maps JSON config to actual props
|
||||
- `getPageShortcuts(featureToggles)` - Extracts keyboard shortcuts
|
||||
|
||||
### 3. `validate-config.ts` - Configuration Validator
|
||||
|
||||
Validates the pages.json configuration for errors and warnings.
|
||||
|
||||
**Location**: `/src/config/validate-config.ts`
|
||||
|
||||
**Validates**:
|
||||
- Required fields (id, title, component)
|
||||
- Duplicate IDs, shortcuts, or order numbers
|
||||
- Valid state and action keys
|
||||
- Resizable configuration correctness
|
||||
- Feature toggle key validity
|
||||
|
||||
### 4. `page-schema.ts` - Type Definitions
|
||||
|
||||
Zod schemas and TypeScript types for type-safe configuration.
|
||||
|
||||
**Location**: `/src/config/page-schema.ts`
|
||||
|
||||
**Exports**:
|
||||
- `PropConfigSchema` - Props configuration structure
|
||||
- `ResizableConfigSchema` - Resizable layout structure
|
||||
- `SimplePageConfigSchema` - Individual page structure
|
||||
- TypeScript types inferred from schemas
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Adding a New Page
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Edit pages.json] --> B[Add page config with props]
|
||||
B --> C[page-loader reads config]
|
||||
C --> D[App.tsx calls resolveProps]
|
||||
D --> E[Props mapped from state/actions]
|
||||
E --> F[Component rendered with props]
|
||||
```
|
||||
|
||||
### Prop Resolution Flow
|
||||
|
||||
```
|
||||
1. Component needs props
|
||||
↓
|
||||
2. App.tsx calls getPropsForComponent(pageId)
|
||||
↓
|
||||
3. getPropsForComponent finds page config
|
||||
↓
|
||||
4. Calls resolveProps(page.props, stateContext, actionContext)
|
||||
↓
|
||||
5. resolveProps iterates over state/action arrays
|
||||
↓
|
||||
6. Maps JSON keys to actual values from contexts
|
||||
↓
|
||||
7. Returns resolved props object
|
||||
↓
|
||||
8. Props passed to component
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Simple Page (Dashboard)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dashboard",
|
||||
"title": "Dashboard",
|
||||
"icon": "ChartBar",
|
||||
"component": "ProjectDashboard",
|
||||
"enabled": true,
|
||||
"shortcut": "ctrl+1",
|
||||
"order": 1,
|
||||
"props": {
|
||||
"state": ["files", "models", "components"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Page with Actions (Model Designer)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "models",
|
||||
"title": "Models",
|
||||
"component": "ModelDesigner",
|
||||
"props": {
|
||||
"state": ["models"],
|
||||
"actions": ["onModelsChange:setModels"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Page with Renamed Props (Flask Designer)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "flask",
|
||||
"component": "FlaskDesigner",
|
||||
"props": {
|
||||
"state": ["config:flaskConfig"],
|
||||
"actions": ["onConfigChange:setFlaskConfig"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resizable Split-Panel Page (Code Editor)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "code",
|
||||
"component": "CodeEditor",
|
||||
"requiresResizable": true,
|
||||
"props": {
|
||||
"state": ["files", "activeFileId"],
|
||||
"actions": ["onFileChange:handleFileChange"]
|
||||
},
|
||||
"resizableConfig": {
|
||||
"leftComponent": "FileExplorer",
|
||||
"leftProps": {
|
||||
"state": ["files"],
|
||||
"actions": ["onFileSelect:setActiveFileId"]
|
||||
},
|
||||
"leftPanel": {
|
||||
"defaultSize": 20,
|
||||
"minSize": 15,
|
||||
"maxSize": 30
|
||||
},
|
||||
"rightPanel": {
|
||||
"defaultSize": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State Context Keys
|
||||
|
||||
Available state variables that can be referenced in `props.state`:
|
||||
|
||||
- `files` - Project files
|
||||
- `models` - Data models
|
||||
- `components` - Component definitions
|
||||
- `componentTrees` - Component tree structures
|
||||
- `workflows` - Workflow definitions
|
||||
- `lambdas` - Lambda functions
|
||||
- `theme` - Theme configuration
|
||||
- `playwrightTests` - Playwright tests
|
||||
- `storybookStories` - Storybook stories
|
||||
- `unitTests` - Unit tests
|
||||
- `flaskConfig` - Flask configuration
|
||||
- `nextjsConfig` - Next.js configuration
|
||||
- `npmSettings` - NPM settings
|
||||
- `featureToggles` - Feature toggles
|
||||
- `activeFileId` - Active file ID
|
||||
|
||||
## Action Context Keys
|
||||
|
||||
Available action functions that can be referenced in `props.actions`:
|
||||
|
||||
- `handleFileChange` - Update file content
|
||||
- `setActiveFileId` - Set active file
|
||||
- `handleFileClose` - Close file
|
||||
- `handleFileAdd` - Add new file
|
||||
- `setModels` - Update models
|
||||
- `setComponents` - Update components
|
||||
- `setComponentTrees` - Update component trees
|
||||
- `setWorkflows` - Update workflows
|
||||
- `setLambdas` - Update lambdas
|
||||
- `setTheme` - Update theme
|
||||
- `setPlaywrightTests` - Update Playwright tests
|
||||
- `setStorybookStories` - Update Storybook stories
|
||||
- `setUnitTests` - Update unit tests
|
||||
- `setFlaskConfig` - Update Flask config
|
||||
- `setNextjsConfig` - Update Next.js config
|
||||
- `setNpmSettings` - Update NPM settings
|
||||
- `setFeatureToggles` - Update feature toggles
|
||||
|
||||
## Benefits of This Architecture
|
||||
|
||||
### 1. **Declarative Configuration**
|
||||
- All pages defined in one place
|
||||
- Easy to understand at a glance
|
||||
- No need to read through TypeScript code
|
||||
|
||||
### 2. **Separation of Concerns**
|
||||
- Configuration separate from logic
|
||||
- Props resolution handled systematically
|
||||
- Business logic stays in components
|
||||
|
||||
### 3. **Type Safety**
|
||||
- Zod schemas validate runtime data
|
||||
- TypeScript types ensure compile-time safety
|
||||
- Catch errors early in development
|
||||
|
||||
### 4. **Maintainability**
|
||||
- Add pages without touching App.tsx logic
|
||||
- Modify props without code changes
|
||||
- Clear validation error messages
|
||||
|
||||
### 5. **Scalability**
|
||||
- Easy to add new pages
|
||||
- Simple to extend prop mappings
|
||||
- Can add more configuration options
|
||||
|
||||
### 6. **Testability**
|
||||
- Validate configuration independently
|
||||
- Test prop resolution in isolation
|
||||
- Mock configurations for testing
|
||||
|
||||
## Validation
|
||||
|
||||
Run configuration validation:
|
||||
|
||||
```bash
|
||||
# Via TypeScript (if configured)
|
||||
npm run validate-config
|
||||
|
||||
# Or manually import and run
|
||||
import { validatePageConfig, printValidationErrors } from '@/config/validate-config'
|
||||
const errors = validatePageConfig()
|
||||
printValidationErrors(errors)
|
||||
```
|
||||
|
||||
Validation checks:
|
||||
- ✅ Required fields present
|
||||
- ✅ No duplicate IDs or shortcuts
|
||||
- ✅ Valid state/action keys
|
||||
- ✅ Correct resizable config format
|
||||
- ✅ Panel sizes sum to 100
|
||||
- ⚠️ Warnings for missing icons or duplicate orders
|
||||
|
||||
## Extending the System
|
||||
|
||||
### Adding New State Keys
|
||||
|
||||
1. Add state to `useProjectState` hook
|
||||
2. Add to `stateContext` in `App.tsx`
|
||||
3. Add to validation in `validate-config.ts`
|
||||
4. Document in this README
|
||||
|
||||
### Adding New Action Keys
|
||||
|
||||
1. Create action function in `App.tsx` or hook
|
||||
2. Add to `actionContext` in `App.tsx`
|
||||
3. Add to validation in `validate-config.ts`
|
||||
4. Document in this README
|
||||
|
||||
### Adding New Configuration Options
|
||||
|
||||
1. Update `pages.json` with new field
|
||||
2. Update `PageConfig` interface in `page-loader.ts`
|
||||
3. Update Zod schema in `page-schema.ts`
|
||||
4. Add validation in `validate-config.ts`
|
||||
5. Implement logic in `App.tsx`
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Hardcoded Props to JSON Config
|
||||
|
||||
**Before** (Hardcoded in App.tsx):
|
||||
```typescript
|
||||
const getPropsForComponent = (pageId: string) => {
|
||||
const propsMap: Record<string, any> = {
|
||||
'ModelDesigner': {
|
||||
models,
|
||||
onModelsChange: setModels,
|
||||
},
|
||||
}
|
||||
return propsMap[pageId] || {}
|
||||
}
|
||||
```
|
||||
|
||||
**After** (Declarative in pages.json):
|
||||
```json
|
||||
{
|
||||
"id": "models",
|
||||
"component": "ModelDesigner",
|
||||
"props": {
|
||||
"state": ["models"],
|
||||
"actions": ["onModelsChange:setModels"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep props minimal** - Only pass what the component needs
|
||||
2. **Use descriptive prop names** - Clear intent over brevity
|
||||
3. **Validate configurations** - Run validation before deployment
|
||||
4. **Document new features** - Update this README when extending
|
||||
5. **Consistent naming** - Follow existing patterns for state/actions
|
||||
6. **Group related props** - Keep state and actions organized
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Props not reaching component?
|
||||
- Check state/action key spelling in pages.json
|
||||
- Verify key exists in stateContext/actionContext
|
||||
- Run validation to catch errors
|
||||
|
||||
### Component not rendering?
|
||||
- Ensure component added to componentMap in App.tsx
|
||||
- Check component name matches exactly (case-sensitive)
|
||||
- Verify component has default export
|
||||
|
||||
### Resizable panel issues?
|
||||
- Ensure defaultSize values sum to 100
|
||||
- Check leftComponent exists in componentMap
|
||||
- Verify resizableConfig structure is complete
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [PROPS_CONFIG_GUIDE.md](./PROPS_CONFIG_GUIDE.md) - Detailed props configuration guide
|
||||
- [src/config/pages.json](./src/config/pages.json) - Current page configurations
|
||||
- [src/config/page-loader.ts](./src/config/page-loader.ts) - Resolution logic
|
||||
- [src/config/validate-config.ts](./src/config/validate-config.ts) - Validation utilities
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements to the configuration system:
|
||||
|
||||
- [ ] Hot-reload configuration changes in development
|
||||
- [ ] Visual configuration editor UI
|
||||
- [ ] Import/export page configurations
|
||||
- [ ] Configuration presets/templates
|
||||
- [ ] Computed/derived props from state
|
||||
- [ ] Conditional prop mappings
|
||||
- [ ] Hook injection via configuration
|
||||
- [ ] Layout templates beyond resizable panels
|
||||
- [ ] Permission-based page visibility
|
||||
- [ ] Analytics tracking configuration
|
||||
317
PROPS_CONFIG_GUIDE.md
Normal file
317
PROPS_CONFIG_GUIDE.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Props Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The CodeForge application now supports **dynamic component props configuration** through the `pages.json` file. This declarative approach eliminates the need to modify `App.tsx` when adding or updating component props, making the system more maintainable and scalable.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Files
|
||||
|
||||
1. **`src/config/pages.json`** - Declarative page and props configuration
|
||||
2. **`src/config/page-loader.ts`** - Props resolution logic and interfaces
|
||||
3. **`src/App.tsx`** - Consumes the configuration and renders pages
|
||||
|
||||
## Props Configuration Schema
|
||||
|
||||
### Basic Props Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "example-page",
|
||||
"component": "ExampleComponent",
|
||||
"props": {
|
||||
"state": ["stateKey1", "stateKey2"],
|
||||
"actions": ["actionPropName:actionFunctionName"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Props Configuration Options
|
||||
|
||||
#### `state` Array
|
||||
|
||||
Maps application state to component props. Supports two formats:
|
||||
|
||||
1. **Direct mapping** (prop name = state key):
|
||||
```json
|
||||
"state": ["files", "models", "theme"]
|
||||
```
|
||||
Results in: `{ files, models, theme }`
|
||||
|
||||
2. **Renamed mapping** (prop name : state key):
|
||||
```json
|
||||
"state": ["trees:componentTrees", "config:flaskConfig"]
|
||||
```
|
||||
Results in: `{ trees: componentTrees, config: flaskConfig }`
|
||||
|
||||
#### `actions` Array
|
||||
|
||||
Maps action functions (handlers/setters) to component props. Uses format:
|
||||
|
||||
```json
|
||||
"actions": ["propName:functionName"]
|
||||
```
|
||||
|
||||
Example:
|
||||
```json
|
||||
"actions": [
|
||||
"onModelsChange:setModels",
|
||||
"onFileSelect:setActiveFileId"
|
||||
]
|
||||
```
|
||||
|
||||
### Resizable Page Configuration
|
||||
|
||||
For pages with split-panel layouts (e.g., File Explorer + Code Editor):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "code",
|
||||
"component": "CodeEditor",
|
||||
"requiresResizable": true,
|
||||
"props": {
|
||||
"state": ["files", "activeFileId"],
|
||||
"actions": ["onFileChange:handleFileChange"]
|
||||
},
|
||||
"resizableConfig": {
|
||||
"leftComponent": "FileExplorer",
|
||||
"leftProps": {
|
||||
"state": ["files", "activeFileId"],
|
||||
"actions": ["onFileSelect:setActiveFileId"]
|
||||
},
|
||||
"leftPanel": {
|
||||
"defaultSize": 20,
|
||||
"minSize": 15,
|
||||
"maxSize": 30
|
||||
},
|
||||
"rightPanel": {
|
||||
"defaultSize": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available State Keys
|
||||
|
||||
The following state variables are available in the state context:
|
||||
|
||||
- `files` - Project files
|
||||
- `models` - Data models
|
||||
- `components` - Component definitions
|
||||
- `componentTrees` - Component tree structures
|
||||
- `workflows` - Workflow definitions
|
||||
- `lambdas` - Lambda function definitions
|
||||
- `theme` - Theme configuration
|
||||
- `playwrightTests` - Playwright test definitions
|
||||
- `storybookStories` - Storybook story definitions
|
||||
- `unitTests` - Unit test definitions
|
||||
- `flaskConfig` - Flask API configuration
|
||||
- `nextjsConfig` - Next.js project configuration
|
||||
- `npmSettings` - NPM package settings
|
||||
- `featureToggles` - Feature toggle states
|
||||
- `activeFileId` - Currently selected file ID
|
||||
|
||||
## Available Actions
|
||||
|
||||
The following action functions are available in the action context:
|
||||
|
||||
- `handleFileChange` - Update file content
|
||||
- `setActiveFileId` - Set active file
|
||||
- `handleFileClose` - Close a file
|
||||
- `handleFileAdd` - Add new file
|
||||
- `setModels` - Update models
|
||||
- `setComponents` - Update components
|
||||
- `setComponentTrees` - Update component trees
|
||||
- `setWorkflows` - Update workflows
|
||||
- `setLambdas` - Update lambdas
|
||||
- `setTheme` - Update theme
|
||||
- `setPlaywrightTests` - Update Playwright tests
|
||||
- `setStorybookStories` - Update Storybook stories
|
||||
- `setUnitTests` - Update unit tests
|
||||
- `setFlaskConfig` - Update Flask config
|
||||
- `setNextjsConfig` - Update Next.js config
|
||||
- `setNpmSettings` - Update NPM settings
|
||||
- `setFeatureToggles` - Update feature toggles
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Component (No Props)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "docs",
|
||||
"title": "Documentation",
|
||||
"component": "DocumentationView",
|
||||
"props": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with State Only
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "models",
|
||||
"title": "Models",
|
||||
"component": "ModelDesigner",
|
||||
"props": {
|
||||
"state": ["models"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with State and Actions
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "models",
|
||||
"title": "Models",
|
||||
"component": "ModelDesigner",
|
||||
"props": {
|
||||
"state": ["models"],
|
||||
"actions": ["onModelsChange:setModels"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with Renamed Props
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "flask",
|
||||
"title": "Flask API",
|
||||
"component": "FlaskDesigner",
|
||||
"props": {
|
||||
"state": ["config:flaskConfig"],
|
||||
"actions": ["onConfigChange:setFlaskConfig"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dashboard with Multiple State Props
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dashboard",
|
||||
"title": "Dashboard",
|
||||
"component": "ProjectDashboard",
|
||||
"props": {
|
||||
"state": [
|
||||
"files",
|
||||
"models",
|
||||
"components",
|
||||
"theme",
|
||||
"playwrightTests",
|
||||
"storybookStories",
|
||||
"unitTests",
|
||||
"flaskConfig"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding a New Page
|
||||
|
||||
To add a new page with props configuration:
|
||||
|
||||
1. **Add the page to `pages.json`**:
|
||||
```json
|
||||
{
|
||||
"id": "new-page",
|
||||
"title": "New Feature",
|
||||
"icon": "Star",
|
||||
"component": "NewFeatureComponent",
|
||||
"enabled": true,
|
||||
"order": 21,
|
||||
"props": {
|
||||
"state": ["relevantState"],
|
||||
"actions": ["onAction:setRelevantState"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add the component to the lazy import map in `App.tsx`**:
|
||||
```typescript
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
// ... existing components
|
||||
NewFeatureComponent: lazy(() => import('@/components/NewFeatureComponent').then(m => ({ default: m.NewFeatureComponent }))),
|
||||
}
|
||||
```
|
||||
|
||||
3. **Optionally add to feature toggles** (if applicable):
|
||||
```json
|
||||
{
|
||||
"toggleKey": "newFeature"
|
||||
}
|
||||
```
|
||||
|
||||
That's it! No need to modify the `getPropsForComponent` function or other logic in `App.tsx`.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Declarative Configuration** - All page configs in one place
|
||||
2. **No Code Changes** - Add/modify pages without touching `App.tsx` logic
|
||||
3. **Type Safety** - TypeScript interfaces ensure configuration validity
|
||||
4. **Maintainability** - Easy to understand and modify page props
|
||||
5. **Scalability** - Simple to add new pages and props
|
||||
6. **Consistency** - Standardized prop resolution across all pages
|
||||
|
||||
## Migration from Old System
|
||||
|
||||
The old hardcoded `propsMap` in `getPropsForComponent` has been replaced with dynamic resolution using `resolveProps()`. The configuration in `pages.json` now drives all prop mapping.
|
||||
|
||||
### Before (Hardcoded in App.tsx)
|
||||
```typescript
|
||||
const propsMap: Record<string, any> = {
|
||||
'ModelDesigner': {
|
||||
models,
|
||||
onModelsChange: setModels,
|
||||
},
|
||||
// ... 20+ more entries
|
||||
}
|
||||
```
|
||||
|
||||
### After (Declarative in pages.json)
|
||||
```json
|
||||
{
|
||||
"id": "models",
|
||||
"component": "ModelDesigner",
|
||||
"props": {
|
||||
"state": ["models"],
|
||||
"actions": ["onModelsChange:setModels"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential extensions to the props configuration system:
|
||||
|
||||
1. **Computed Props** - Props derived from multiple state values
|
||||
2. **Prop Transformations** - Map/filter/reduce operations on state
|
||||
3. **Conditional Props** - Props based on feature toggles or user state
|
||||
4. **Default Values** - Fallback values for missing state
|
||||
5. **Validation Rules** - Runtime prop validation from schema
|
||||
6. **Hook Configuration** - Custom hooks to inject into components
|
||||
7. **Event Handlers** - Declarative event handler composition
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Props not being passed to component
|
||||
|
||||
1. Check that the state/action key exists in the context objects
|
||||
2. Verify the prop name mapping is correct (before:after colon)
|
||||
3. Ensure the page is enabled and not filtered by feature toggles
|
||||
4. Check browser console for resolution errors
|
||||
|
||||
### Component not rendering
|
||||
|
||||
1. Verify the component is added to the `componentMap` in `App.tsx`
|
||||
2. Check that the component name matches exactly (case-sensitive)
|
||||
3. Ensure the component export matches the import pattern
|
||||
|
||||
### State updates not working
|
||||
|
||||
1. Verify the action function name is correct
|
||||
2. Check that the setter is using functional updates for array/object state
|
||||
3. Ensure the action is properly mapped in the action context
|
||||
199
src/App.tsx
199
src/App.tsx
@@ -6,7 +6,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/componen
|
||||
import { useProjectState } from '@/hooks/use-project-state'
|
||||
import { useFileOperations } from '@/hooks/use-file-operations'
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||
import { getPageConfig, getEnabledPages, getPageShortcuts } from '@/config/page-loader'
|
||||
import { getPageConfig, getEnabledPages, getPageShortcuts, resolveProps } from '@/config/page-loader'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
|
||||
@@ -134,92 +134,48 @@ function App() {
|
||||
}
|
||||
|
||||
const getPropsForComponent = (pageId: string) => {
|
||||
const propsMap: Record<string, any> = {
|
||||
'ProjectDashboard': {
|
||||
files,
|
||||
models,
|
||||
components,
|
||||
theme,
|
||||
playwrightTests,
|
||||
storybookStories,
|
||||
unitTests,
|
||||
flaskConfig,
|
||||
},
|
||||
'CodeEditor': {
|
||||
files,
|
||||
activeFileId,
|
||||
onFileChange: handleFileChange,
|
||||
onFileSelect: setActiveFileId,
|
||||
onFileClose: handleFileClose,
|
||||
},
|
||||
'FileExplorer': {
|
||||
files,
|
||||
activeFileId,
|
||||
onFileSelect: setActiveFileId,
|
||||
onFileAdd: handleFileAdd,
|
||||
},
|
||||
'ModelDesigner': {
|
||||
models,
|
||||
onModelsChange: setModels,
|
||||
},
|
||||
'ComponentTreeBuilder': {
|
||||
components,
|
||||
onComponentsChange: setComponents,
|
||||
},
|
||||
'ComponentTreeManager': {
|
||||
trees: componentTrees,
|
||||
onTreesChange: setComponentTrees,
|
||||
},
|
||||
'WorkflowDesigner': {
|
||||
workflows,
|
||||
onWorkflowsChange: setWorkflows,
|
||||
},
|
||||
'LambdaDesigner': {
|
||||
lambdas,
|
||||
onLambdasChange: setLambdas,
|
||||
},
|
||||
'StyleDesigner': {
|
||||
theme,
|
||||
onThemeChange: setTheme,
|
||||
},
|
||||
'FlaskDesigner': {
|
||||
config: flaskConfig,
|
||||
onConfigChange: setFlaskConfig,
|
||||
},
|
||||
'PlaywrightDesigner': {
|
||||
tests: playwrightTests,
|
||||
onTestsChange: setPlaywrightTests,
|
||||
},
|
||||
'StorybookDesigner': {
|
||||
stories: storybookStories,
|
||||
onStoriesChange: setStorybookStories,
|
||||
},
|
||||
'UnitTestDesigner': {
|
||||
tests: unitTests,
|
||||
onTestsChange: setUnitTests,
|
||||
},
|
||||
'ErrorPanel': {
|
||||
files,
|
||||
onFileChange: handleFileChange,
|
||||
onFileSelect: setActiveFileId,
|
||||
},
|
||||
'ProjectSettingsDesigner': {
|
||||
nextjsConfig,
|
||||
npmSettings,
|
||||
onNextjsConfigChange: setNextjsConfig,
|
||||
onNpmSettingsChange: setNpmSettings,
|
||||
},
|
||||
'FeatureToggleSettings': {
|
||||
features: featureToggles,
|
||||
onFeaturesChange: setFeatureToggles,
|
||||
},
|
||||
'DocumentationView': {},
|
||||
'SassStylesShowcase': {},
|
||||
'PWASettings': {},
|
||||
'FaviconDesigner': {},
|
||||
'FeatureIdeaCloud': {},
|
||||
const page = enabledPages.find(p => p.id === pageId)
|
||||
if (!page || !page.props) return {}
|
||||
|
||||
const stateContext = {
|
||||
files,
|
||||
models,
|
||||
components,
|
||||
componentTrees,
|
||||
workflows,
|
||||
lambdas,
|
||||
theme,
|
||||
playwrightTests,
|
||||
storybookStories,
|
||||
unitTests,
|
||||
flaskConfig,
|
||||
nextjsConfig,
|
||||
npmSettings,
|
||||
featureToggles,
|
||||
activeFileId,
|
||||
}
|
||||
return propsMap[pageId] || {}
|
||||
|
||||
const actionContext = {
|
||||
handleFileChange,
|
||||
setActiveFileId,
|
||||
handleFileClose,
|
||||
handleFileAdd,
|
||||
setModels,
|
||||
setComponents,
|
||||
setComponentTrees,
|
||||
setWorkflows,
|
||||
setLambdas,
|
||||
setTheme,
|
||||
setPlaywrightTests,
|
||||
setStorybookStories,
|
||||
setUnitTests,
|
||||
setFlaskConfig,
|
||||
setNextjsConfig,
|
||||
setNpmSettings,
|
||||
setFeatureToggles,
|
||||
}
|
||||
|
||||
return resolveProps(page.props, stateContext, actionContext)
|
||||
}
|
||||
|
||||
const renderPageContent = (page: any) => {
|
||||
@@ -228,27 +184,78 @@ function App() {
|
||||
return <LoadingFallback message={`Component ${page.component} not found`} />
|
||||
}
|
||||
|
||||
if (page.requiresResizable && page.id === 'code') {
|
||||
const FileExplorerComp = componentMap['FileExplorer']
|
||||
const CodeEditorComp = componentMap['CodeEditor']
|
||||
if (page.requiresResizable && page.resizableConfig) {
|
||||
const config = page.resizableConfig
|
||||
const LeftComponent = componentMap[config.leftComponent]
|
||||
const RightComponent = Component
|
||||
|
||||
if (!LeftComponent) {
|
||||
return <LoadingFallback message={`Component ${config.leftComponent} not found`} />
|
||||
}
|
||||
|
||||
const stateContext = {
|
||||
files,
|
||||
models,
|
||||
components,
|
||||
componentTrees,
|
||||
workflows,
|
||||
lambdas,
|
||||
theme,
|
||||
playwrightTests,
|
||||
storybookStories,
|
||||
unitTests,
|
||||
flaskConfig,
|
||||
nextjsConfig,
|
||||
npmSettings,
|
||||
featureToggles,
|
||||
activeFileId,
|
||||
}
|
||||
|
||||
const actionContext = {
|
||||
handleFileChange,
|
||||
setActiveFileId,
|
||||
handleFileClose,
|
||||
handleFileAdd,
|
||||
setModels,
|
||||
setComponents,
|
||||
setComponentTrees,
|
||||
setWorkflows,
|
||||
setLambdas,
|
||||
setTheme,
|
||||
setPlaywrightTests,
|
||||
setStorybookStories,
|
||||
setUnitTests,
|
||||
setFlaskConfig,
|
||||
setNextjsConfig,
|
||||
setNpmSettings,
|
||||
setFeatureToggles,
|
||||
}
|
||||
|
||||
const leftProps = resolveProps(config.leftProps, stateContext, actionContext)
|
||||
const rightProps = getPropsForComponent(page.id)
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
|
||||
<Suspense fallback={<LoadingFallback message="Loading explorer..." />}>
|
||||
<FileExplorerComp {...getPropsForComponent('FileExplorer')} />
|
||||
<ResizablePanel
|
||||
defaultSize={config.leftPanel.defaultSize}
|
||||
minSize={config.leftPanel.minSize}
|
||||
maxSize={config.leftPanel.maxSize}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback message={`Loading ${config.leftComponent.toLowerCase()}...`} />}>
|
||||
<LeftComponent {...leftProps} />
|
||||
</Suspense>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={80}>
|
||||
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
|
||||
<CodeEditorComp {...getPropsForComponent('CodeEditor')} />
|
||||
<ResizablePanel defaultSize={config.rightPanel.defaultSize}>
|
||||
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
|
||||
<RightComponent {...rightProps} />
|
||||
</Suspense>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const props = getPropsForComponent(page.component)
|
||||
const props = getPropsForComponent(page.id)
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
|
||||
<Component {...props} />
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface PropConfig {
|
||||
state?: string[]
|
||||
actions?: string[]
|
||||
}
|
||||
|
||||
export interface ResizableConfig {
|
||||
leftComponent: string
|
||||
leftProps: PropConfig
|
||||
leftPanel: {
|
||||
defaultSize: number
|
||||
minSize: number
|
||||
maxSize: number
|
||||
}
|
||||
rightPanel: {
|
||||
defaultSize: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
title: string
|
||||
@@ -11,6 +29,8 @@ export interface PageConfig {
|
||||
shortcut?: string
|
||||
order: number
|
||||
requiresResizable?: boolean
|
||||
props?: PropConfig
|
||||
resizableConfig?: ResizableConfig
|
||||
}
|
||||
|
||||
export interface PagesConfig {
|
||||
@@ -57,3 +77,33 @@ export function getPageShortcuts(featureToggles?: FeatureToggles): Array<{
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveProps(propConfig: PropConfig | undefined, stateContext: Record<string, any>, actionContext: Record<string, any>): Record<string, any> {
|
||||
if (!propConfig) return {}
|
||||
|
||||
const resolvedProps: Record<string, any> = {}
|
||||
|
||||
if (propConfig.state) {
|
||||
for (const stateKey of propConfig.state) {
|
||||
const [propName, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
if (stateContext[contextKey] !== undefined) {
|
||||
resolvedProps[propName] = stateContext[contextKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (propConfig.actions) {
|
||||
for (const actionKey of propConfig.actions) {
|
||||
const [propName, contextKey] = actionKey.split(':')
|
||||
|
||||
if (actionContext[contextKey]) {
|
||||
resolvedProps[propName] = actionContext[contextKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedProps
|
||||
}
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const PropConfigSchema = z.object({
|
||||
state: z.array(z.string()).optional(),
|
||||
actions: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const ResizablePanelConfigSchema = z.object({
|
||||
defaultSize: z.number(),
|
||||
minSize: z.number().optional(),
|
||||
maxSize: z.number().optional(),
|
||||
})
|
||||
|
||||
export const ResizableConfigSchema = z.object({
|
||||
leftComponent: z.string(),
|
||||
leftProps: PropConfigSchema,
|
||||
leftPanel: ResizablePanelConfigSchema,
|
||||
rightPanel: ResizablePanelConfigSchema,
|
||||
})
|
||||
|
||||
export const SimplePageConfigSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
icon: z.string(),
|
||||
component: z.string(),
|
||||
enabled: z.boolean(),
|
||||
toggleKey: z.string().optional(),
|
||||
shortcut: z.string().optional(),
|
||||
order: z.number(),
|
||||
requiresResizable: z.boolean().optional(),
|
||||
props: PropConfigSchema.optional(),
|
||||
resizableConfig: ResizableConfigSchema.optional(),
|
||||
})
|
||||
|
||||
export const SimplePagesConfigSchema = z.object({
|
||||
pages: z.array(SimplePageConfigSchema),
|
||||
})
|
||||
|
||||
export const KeyboardShortcutSchema = z.object({
|
||||
key: z.string(),
|
||||
ctrl: z.boolean().optional(),
|
||||
@@ -45,6 +81,11 @@ export const PageRegistrySchema = z.object({
|
||||
pages: z.array(PageConfigSchema),
|
||||
})
|
||||
|
||||
export type PropConfig = z.infer<typeof PropConfigSchema>
|
||||
export type ResizablePanelConfig = z.infer<typeof ResizablePanelConfigSchema>
|
||||
export type ResizableConfig = z.infer<typeof ResizableConfigSchema>
|
||||
export type SimplePageConfig = z.infer<typeof SimplePageConfigSchema>
|
||||
export type SimplePagesConfig = z.infer<typeof SimplePagesConfigSchema>
|
||||
export type KeyboardShortcut = z.infer<typeof KeyboardShortcutSchema>
|
||||
export type PanelConfig = z.infer<typeof PanelConfigSchema>
|
||||
export type LayoutConfig = z.infer<typeof LayoutConfigSchema>
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"component": "ProjectDashboard",
|
||||
"enabled": true,
|
||||
"shortcut": "ctrl+1",
|
||||
"order": 1
|
||||
"order": 1,
|
||||
"props": {
|
||||
"state": ["files", "models", "components", "theme", "playwrightTests", "storybookStories", "unitTests", "flaskConfig"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "code",
|
||||
@@ -18,7 +21,26 @@
|
||||
"toggleKey": "codeEditor",
|
||||
"shortcut": "ctrl+2",
|
||||
"order": 2,
|
||||
"requiresResizable": true
|
||||
"requiresResizable": true,
|
||||
"props": {
|
||||
"state": ["files", "activeFileId"],
|
||||
"actions": ["onFileChange:handleFileChange", "onFileSelect:setActiveFileId", "onFileClose:handleFileClose"]
|
||||
},
|
||||
"resizableConfig": {
|
||||
"leftComponent": "FileExplorer",
|
||||
"leftProps": {
|
||||
"state": ["files", "activeFileId"],
|
||||
"actions": ["onFileSelect:setActiveFileId", "onFileAdd:handleFileAdd"]
|
||||
},
|
||||
"leftPanel": {
|
||||
"defaultSize": 20,
|
||||
"minSize": 15,
|
||||
"maxSize": 30
|
||||
},
|
||||
"rightPanel": {
|
||||
"defaultSize": 80
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "models",
|
||||
@@ -28,7 +50,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "models",
|
||||
"shortcut": "ctrl+3",
|
||||
"order": 3
|
||||
"order": 3,
|
||||
"props": {
|
||||
"state": ["models"],
|
||||
"actions": ["onModelsChange:setModels"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "components",
|
||||
@@ -38,7 +64,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "components",
|
||||
"shortcut": "ctrl+4",
|
||||
"order": 4
|
||||
"order": 4,
|
||||
"props": {
|
||||
"state": ["components"],
|
||||
"actions": ["onComponentsChange:setComponents"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "component-trees",
|
||||
@@ -48,7 +78,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "componentTrees",
|
||||
"shortcut": "ctrl+5",
|
||||
"order": 5
|
||||
"order": 5,
|
||||
"props": {
|
||||
"state": ["trees:componentTrees"],
|
||||
"actions": ["onTreesChange:setComponentTrees"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workflows",
|
||||
@@ -58,7 +92,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "workflows",
|
||||
"shortcut": "ctrl+6",
|
||||
"order": 6
|
||||
"order": 6,
|
||||
"props": {
|
||||
"state": ["workflows"],
|
||||
"actions": ["onWorkflowsChange:setWorkflows"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lambdas",
|
||||
@@ -68,7 +106,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "lambdas",
|
||||
"shortcut": "ctrl+7",
|
||||
"order": 7
|
||||
"order": 7,
|
||||
"props": {
|
||||
"state": ["lambdas"],
|
||||
"actions": ["onLambdasChange:setLambdas"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "styling",
|
||||
@@ -78,7 +120,11 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "styling",
|
||||
"shortcut": "ctrl+8",
|
||||
"order": 8
|
||||
"order": 8,
|
||||
"props": {
|
||||
"state": ["theme"],
|
||||
"actions": ["onThemeChange:setTheme"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "favicon",
|
||||
@@ -88,7 +134,8 @@
|
||||
"enabled": true,
|
||||
"toggleKey": "faviconDesigner",
|
||||
"shortcut": "ctrl+9",
|
||||
"order": 9
|
||||
"order": 9,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "ideas",
|
||||
@@ -97,7 +144,8 @@
|
||||
"component": "FeatureIdeaCloud",
|
||||
"enabled": true,
|
||||
"toggleKey": "ideaCloud",
|
||||
"order": 10
|
||||
"order": 10,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "flask",
|
||||
@@ -106,7 +154,11 @@
|
||||
"component": "FlaskDesigner",
|
||||
"enabled": true,
|
||||
"toggleKey": "flaskApi",
|
||||
"order": 11
|
||||
"order": 11,
|
||||
"props": {
|
||||
"state": ["config:flaskConfig"],
|
||||
"actions": ["onConfigChange:setFlaskConfig"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "playwright",
|
||||
@@ -115,7 +167,11 @@
|
||||
"component": "PlaywrightDesigner",
|
||||
"enabled": true,
|
||||
"toggleKey": "playwright",
|
||||
"order": 12
|
||||
"order": 12,
|
||||
"props": {
|
||||
"state": ["tests:playwrightTests"],
|
||||
"actions": ["onTestsChange:setPlaywrightTests"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storybook",
|
||||
@@ -124,7 +180,11 @@
|
||||
"component": "StorybookDesigner",
|
||||
"enabled": true,
|
||||
"toggleKey": "storybook",
|
||||
"order": 13
|
||||
"order": 13,
|
||||
"props": {
|
||||
"state": ["stories:storybookStories"],
|
||||
"actions": ["onStoriesChange:setStorybookStories"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unit-tests",
|
||||
@@ -133,7 +193,11 @@
|
||||
"component": "UnitTestDesigner",
|
||||
"enabled": true,
|
||||
"toggleKey": "unitTests",
|
||||
"order": 14
|
||||
"order": 14,
|
||||
"props": {
|
||||
"state": ["tests:unitTests"],
|
||||
"actions": ["onTestsChange:setUnitTests"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "errors",
|
||||
@@ -142,7 +206,11 @@
|
||||
"component": "ErrorPanel",
|
||||
"enabled": true,
|
||||
"toggleKey": "errorRepair",
|
||||
"order": 15
|
||||
"order": 15,
|
||||
"props": {
|
||||
"state": ["files"],
|
||||
"actions": ["onFileChange:handleFileChange", "onFileSelect:setActiveFileId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "docs",
|
||||
@@ -151,7 +219,8 @@
|
||||
"component": "DocumentationView",
|
||||
"enabled": true,
|
||||
"toggleKey": "documentation",
|
||||
"order": 16
|
||||
"order": 16,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "sass",
|
||||
@@ -160,7 +229,8 @@
|
||||
"component": "SassStylesShowcase",
|
||||
"enabled": true,
|
||||
"toggleKey": "sassStyles",
|
||||
"order": 17
|
||||
"order": 17,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
@@ -168,7 +238,11 @@
|
||||
"icon": "Gear",
|
||||
"component": "ProjectSettingsDesigner",
|
||||
"enabled": true,
|
||||
"order": 18
|
||||
"order": 18,
|
||||
"props": {
|
||||
"state": ["nextjsConfig", "npmSettings"],
|
||||
"actions": ["onNextjsConfigChange:setNextjsConfig", "onNpmSettingsChange:setNpmSettings"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pwa",
|
||||
@@ -176,7 +250,8 @@
|
||||
"icon": "DeviceMobile",
|
||||
"component": "PWASettings",
|
||||
"enabled": true,
|
||||
"order": 19
|
||||
"order": 19,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "features",
|
||||
@@ -184,7 +259,11 @@
|
||||
"icon": "ToggleRight",
|
||||
"component": "FeatureToggleSettings",
|
||||
"enabled": true,
|
||||
"order": 20
|
||||
"order": 20,
|
||||
"props": {
|
||||
"state": ["features:featureToggles"],
|
||||
"actions": ["onFeaturesChange:setFeatureToggles"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,21 @@ export function validatePageConfig(): ValidationError[] {
|
||||
const seenIds = new Set<string>()
|
||||
const seenShortcuts = new Set<string>()
|
||||
const seenOrders = new Set<number>()
|
||||
|
||||
const validStateKeys = [
|
||||
'files', 'models', 'components', 'componentTrees', 'workflows',
|
||||
'lambdas', 'theme', 'playwrightTests', 'storybookStories',
|
||||
'unitTests', 'flaskConfig', 'nextjsConfig', 'npmSettings',
|
||||
'featureToggles', 'activeFileId'
|
||||
]
|
||||
|
||||
const validActionKeys = [
|
||||
'handleFileChange', 'setActiveFileId', 'handleFileClose', 'handleFileAdd',
|
||||
'setModels', 'setComponents', 'setComponentTrees', 'setWorkflows',
|
||||
'setLambdas', 'setTheme', 'setPlaywrightTests', 'setStorybookStories',
|
||||
'setUnitTests', 'setFlaskConfig', 'setNextjsConfig', 'setNpmSettings',
|
||||
'setFeatureToggles'
|
||||
]
|
||||
|
||||
pagesConfig.pages.forEach((page: PageConfig) => {
|
||||
if (!page.id) {
|
||||
@@ -125,6 +140,82 @@ export function validatePageConfig(): ValidationError[] {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (page.props) {
|
||||
if (page.props.state) {
|
||||
page.props.state.forEach(stateKey => {
|
||||
const [, contextKey] = stateKey.includes(':')
|
||||
? stateKey.split(':')
|
||||
: [stateKey, stateKey]
|
||||
|
||||
if (!validStateKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.state',
|
||||
message: `Unknown state key: ${contextKey}. Valid keys: ${validStateKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (page.props.actions) {
|
||||
page.props.actions.forEach(actionKey => {
|
||||
const [, contextKey] = actionKey.split(':')
|
||||
|
||||
if (!contextKey) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.actions',
|
||||
message: `Action key must use format "propName:functionName". Got: ${actionKey}`,
|
||||
severity: 'error',
|
||||
})
|
||||
} else if (!validActionKeys.includes(contextKey)) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'props.actions',
|
||||
message: `Unknown action key: ${contextKey}. Valid keys: ${validActionKeys.join(', ')}`,
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (page.requiresResizable) {
|
||||
if (!page.resizableConfig) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'resizableConfig',
|
||||
message: 'resizableConfig is required when requiresResizable is true',
|
||||
severity: 'error',
|
||||
})
|
||||
} else {
|
||||
if (!page.resizableConfig.leftComponent) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'resizableConfig.leftComponent',
|
||||
message: 'leftComponent is required in resizableConfig',
|
||||
severity: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
const leftPanel = page.resizableConfig.leftPanel
|
||||
const rightPanel = page.resizableConfig.rightPanel
|
||||
|
||||
if (leftPanel && rightPanel) {
|
||||
const totalSize = leftPanel.defaultSize + rightPanel.defaultSize
|
||||
if (totalSize !== 100) {
|
||||
errors.push({
|
||||
page: page.id || 'Unknown',
|
||||
field: 'resizableConfig',
|
||||
message: `Panel defaultSize values must sum to 100. Got: ${totalSize}`,
|
||||
severity: 'warning',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
|
||||
Reference in New Issue
Block a user