diff --git a/docs-issue-businessprocessselection-refactor.md b/docs-issue-businessprocessselection-refactor.md new file mode 100644 index 0000000000..5ed58c1f5f --- /dev/null +++ b/docs-issue-businessprocessselection-refactor.md @@ -0,0 +1,305 @@ +# Issue: Update BusinessProcessSelection Component to Follow Standard Page Framework Pattern + +## Overview + +The BusinessProcessSelection component currently uses a custom `useDAKUrlParams` hook instead of the standard page framework pattern. This issue tracks the work to migrate it to use the standard `usePage()` hook and follow the wrapper + content component pattern for consistency and maintainability. + +## Current State + +### Implementation +The component currently: +- Uses custom `useDAKUrlParams` hook for accessing page context +- Does not follow the wrapper + content pattern +- Calls hooks before PageLayout wrapping +- Works correctly but differs from the standard pattern used by other DAK components + +### Code Location +- **File**: `src/components/BusinessProcessSelection.js` +- **Pattern**: Custom hook usage with direct PageLayout wrapping + +### Current Code Structure +```javascript +const BusinessProcessSelection = () => { + const { profile, repository, selectedBranch } = useDAKUrlParams(); + // Component logic... + + return ( + + {/* Content */} + + ); +}; +``` + +## Problem Statement + +### Issues with Current Implementation + +1. **Pattern Inconsistency**: Does not follow the standard wrapper + content pattern used by other DAK components (DAKDashboard, ActorEditor, CoreDataDictionaryViewer, etc.) + +2. **Framework Integration**: Uses custom `useDAKUrlParams` hook instead of the page framework's standard `usePage()` hook + +3. **Maintainability Risk**: Custom patterns make the codebase harder to maintain and understand for new developers + +4. **Future Compatibility**: May be affected by future changes to the page framework if it doesn't follow standard patterns + +5. **Documentation Compliance**: Does not comply with REQ-ARCH-001, REQ-ARCH-002, REQ-ARCH-003 requirements established for component architecture + +### Context + +This issue was identified during an audit of all DAK components following the fix for ActorEditor page load failure (Issue #1076). The audit revealed that all other DAK components follow the standard pattern except BusinessProcessSelection. + +## Requirements + +### Functional Requirements + +**REQ-BPS-001**: Component MUST follow the wrapper + content pattern +- Split BusinessProcessSelection into wrapper and content components +- Wrapper component only wraps PageLayout +- Content component contains all logic and state + +**REQ-BPS-002**: Component MUST use standard page framework hooks +- Replace `useDAKUrlParams` with `usePage()` hook +- Access page context through standard framework interface +- Remove dependency on custom hook + +**REQ-BPS-003**: Component MUST maintain existing functionality +- All current features must continue to work as before +- No regression in user experience +- No breaking changes to external interfaces + +**REQ-BPS-004**: Component MUST handle loading and error states +- Properly handle loading state from page context +- Display appropriate error messages when context is unavailable +- Gracefully degrade when required data is missing + +### Non-Functional Requirements + +**REQ-BPS-NF-001**: Code quality and consistency +- Follow the same pattern as DAKDashboard reference implementation +- Maintain or improve code readability +- Add appropriate comments where needed + +**REQ-BPS-NF-002**: Documentation compliance +- Update component to comply with REQ-ARCH-001, REQ-ARCH-002, REQ-ARCH-003 +- Ensure component matches patterns documented in page-framework.md +- Update component-architecture-audit.md to reflect changes + +**REQ-BPS-NF-003**: Testing requirements +- Verify component loads correctly via URL navigation +- Test loading and error states +- Verify BPMN file listing and preview functionality +- Test branch switching if applicable + +## Proposed Solution + +### Target Architecture + +```javascript +// Wrapper component - exports this +const BusinessProcessSelection = () => { + return ( + + + + ); +}; + +// Content component - contains all logic and hooks +const BusinessProcessSelectionContent = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { profile, repository, branch, loading, error } = usePage(); + + // Handle loading state + if (loading) { + return ( +
+

Loading...

+

Initializing business process selection...

+
+ ); + } + + // Handle error state + if (error) { + return ( +
+

Error

+

{error}

+
+ ); + } + + // Component state and logic + const [bpmnFiles, setBpmnFiles] = useState([]); + const [loading, setLoading] = useState(true); + // ... rest of component logic + + return ( +
+ {/* Component UI */} +
+ ); +}; + +export default BusinessProcessSelection; +``` + +### Migration Steps + +1. **Create Content Component** + - Extract all logic from BusinessProcessSelection into BusinessProcessSelectionContent + - Move all state, effects, and handlers to content component + +2. **Update Hook Usage** + - Replace `useDAKUrlParams()` with `usePage()` + - Map `selectedBranch` to `branch` from page context + - Ensure all page context properties are correctly accessed + +3. **Add Error Handling** + - Add loading state handling + - Add error state handling + - Handle missing profile/repository gracefully + +4. **Update Wrapper Component** + - Simplify wrapper to only wrap PageLayout + - Remove all hooks from wrapper + - Export wrapper as default + +5. **Test Thoroughly** + - Test URL navigation to component + - Verify BPMN file listing works + - Test preview functionality + - Verify no console errors + +6. **Update Documentation** + - Update component-architecture-audit.md + - Mark BusinessProcessSelection as compliant + - Remove from "needs review" section + +## Testing Criteria + +### Acceptance Criteria + +✅ Component follows wrapper + content pattern +✅ Uses `usePage()` hook instead of `useDAKUrlParams` +✅ Handles loading state appropriately +✅ Handles error state appropriately +✅ All existing functionality works as before +✅ No console errors when navigating to component +✅ BPMN file listing displays correctly +✅ Preview functionality works +✅ Component-architecture-audit.md updated + +### Test Scenarios + +1. **URL Navigation Test** + - Navigate to: `/business-process-selection/{user}/{repo}/{branch}` + - Verify: Component loads without errors + - Check: No "PageContext is null" errors in console + +2. **Loading State Test** + - Navigate to component with valid DAK + - Verify: Loading state displays while data loads + - Check: Loading completes and files are displayed + +3. **Error State Test** + - Navigate to component with invalid repo + - Verify: Error state displays with appropriate message + - Check: No JavaScript errors in console + +4. **BPMN Listing Test** + - Navigate to component with DAK containing BPMN files + - Verify: BPMN files are listed correctly + - Check: File names and metadata display properly + +5. **Preview Test** + - Click on a BPMN file in the list + - Verify: Preview opens and displays diagram + - Check: Preview functionality works as before + +6. **Branch Context Test** + - Navigate to component with specific branch + - Verify: Correct branch is used for file listing + - Check: Branch context is maintained + +## Priority and Effort + +### Priority +**Low** - The component currently works correctly. This is a technical debt/consistency improvement rather than a bug fix. + +### Effort Estimate +**Small** (2-4 hours) +- Code changes: 1-2 hours +- Testing: 1 hour +- Documentation updates: 0.5 hours +- Code review: 0.5 hours + +### Dependencies +None - This is an isolated change to a single component + +## References + +### Related Documentation +- **Page Framework Documentation**: `public/docs/page-framework.md` (Component Architecture Patterns section) +- **Requirements**: `public/docs/requirements.md` (REQ-ARCH-001, REQ-ARCH-002, REQ-ARCH-003) +- **Component Audit**: `public/docs/component-architecture-audit.md` + +### Related Issues +- Issue #1076: ActorEditor page load failure (demonstrates the issue this pattern prevents) + +### Reference Implementations +- `src/components/DAKDashboard.js` - Reference implementation +- `src/components/ActorEditor.js` - Recently updated to follow pattern +- `src/components/CoreDataDictionaryViewer.js` - Compliant implementation +- `src/components/PersonaViewer.js` - Compliant implementation + +## Implementation Notes + +### Custom Hook Migration + +The `useDAKUrlParams` hook may be used elsewhere in the codebase. Before removing it: +1. Search for all usages: `grep -r "useDAKUrlParams" src/` +2. If other components use it, migrate them first or keep the hook +3. If only BusinessProcessSelection uses it, the hook can be deprecated + +### Backward Compatibility + +Since this is an internal refactoring: +- No API changes +- No prop changes +- No external interface changes +- Component can be safely updated without affecting consumers + +### Code Review Checklist + +- [ ] Wrapper component only wraps PageLayout +- [ ] Content component uses `usePage()` hook +- [ ] Loading state is handled +- [ ] Error state is handled +- [ ] All hooks are in content component +- [ ] Component exports wrapper (not content) +- [ ] Existing functionality works +- [ ] No console errors +- [ ] Documentation updated + +## Success Criteria + +The issue will be considered complete when: + +1. ✅ BusinessProcessSelection follows wrapper + content pattern +2. ✅ Component uses `usePage()` hook +3. ✅ All tests pass +4. ✅ No regression in functionality +5. ✅ Documentation updated +6. ✅ Code review approved +7. ✅ Component-architecture-audit.md marks component as compliant + +## Notes + +- This is a **refactoring task**, not a bug fix +- **Priority is low** - can be completed as part of routine maintenance +- Changes are **isolated** to single component +- **No breaking changes** - internal refactoring only +- **Low risk** - existing tests will catch any regressions diff --git a/public/docs/component-architecture-audit.md b/public/docs/component-architecture-audit.md new file mode 100644 index 0000000000..e35d972c80 --- /dev/null +++ b/public/docs/component-architecture-audit.md @@ -0,0 +1,182 @@ +# Component Architecture Audit + +## Overview + +This document identifies SGEX Workbench components and their compliance with the wrapper + content architecture pattern required for proper PageProvider context initialization. + +**Last Updated**: 2025-10-10 + +## Architecture Pattern Requirement + +All DAK components that use `PageLayout` MUST follow the wrapper + content pattern: + +```javascript +// ✅ CORRECT PATTERN +const MyComponent = () => { + return ( + + + + ); +}; + +const MyComponentContent = () => { + const { profile, repository, branch } = usePage(); + // Component logic here +}; + +export default MyComponent; +``` + +**Why**: This ensures `PageProvider` context exists before any hooks try to access it, preventing "PageContext is null" errors. + +## Component Status + +### ✅ Compliant Components + +These components correctly implement the wrapper + content pattern: + +| Component | File | Status | Notes | +|-----------|------|--------|-------| +| ActorEditor | `src/components/ActorEditor.js` | ✅ Fixed | Recently fixed in PR #1076 | +| CoreDataDictionaryViewer | `src/components/CoreDataDictionaryViewer.js` | ✅ Compliant | Proper wrapper + content pattern | +| ComponentEditor | `src/components/ComponentEditor.js` | ✅ Compliant | Proper wrapper + content pattern | +| PersonaViewer | `src/components/PersonaViewer.js` | ✅ Compliant | Proper wrapper + content pattern | +| DAKDashboard | `src/components/DAKDashboard.js` | ✅ Compliant | Reference implementation | +| QuestionnaireEditor | `src/components/QuestionnaireEditor.js` | ✅ Compliant | Proper wrapper + content pattern | +| DecisionSupportLogicView | `src/components/DecisionSupportLogicView.js` | ✅ Compliant | Proper wrapper + content pattern | + +### ⚠️ Components Needing Review + +These components may need updates or review: + +| Component | File | Issue | Recommendation | +|-----------|------|-------|----------------| +| BusinessProcessSelection | `src/components/BusinessProcessSelection.js` | Uses `useDAKUrlParams` instead of page framework hooks | Consider migrating to `usePage()` hook for consistency | + +### ℹ️ Components Using Alternative Patterns + +These components use different patterns that don't require wrapper + content: + +| Component | File | Pattern | Notes | +|-----------|------|---------|-------| +| BPMNEditor | `src/components/BPMNEditor.js` | AssetEditorLayout | Uses `AssetEditorLayout` which handles context internally | +| BPMNSource | `src/components/BPMNSource.js` | AssetEditorLayout | Uses `AssetEditorLayout` which handles context internally | +| BPMNViewer | `src/components/BPMNViewer.js` | AssetEditorLayout | Uses `AssetEditorLayout` which handles context internally | + +**Note**: `AssetEditorLayout` provides PageProvider internally, so components using it can call `usePage()` directly without the wrapper pattern. + +### 📋 Non-DAK Components + +These components don't use DAK page context and don't require the pattern: + +- WelcomePage +- SelectProfilePage +- DAKActionSelection +- DAKSelection +- DAKConfiguration +- OrganizationSelection +- RepositorySelection +- DashboardRedirect +- LandingPage +- DocumentationViewer +- BranchListingPage +- BranchDeploymentSelector +- DAKFAQDemo + +## Detailed Review: BusinessProcessSelection + +### Current Implementation + +```javascript +const BusinessProcessSelection = () => { + const { profile, repository, selectedBranch } = useDAKUrlParams(); + // Component logic... + + return ( + + {/* Content */} + + ); +}; +``` + +### Issues + +1. Uses custom `useDAKUrlParams` hook instead of framework's `usePage()` hook +2. Calls hook before PageLayout (potential for future issues if useDAKUrlParams changes) +3. Not following the standard pattern used by other DAK components + +### Recommendation + +Consider refactoring to use the standard pattern: + +```javascript +const BusinessProcessSelection = () => { + return ( + + + + ); +}; + +const BusinessProcessSelectionContent = () => { + const { profile, repository, branch } = usePage(); + // Component logic... +}; +``` + +**Priority**: Low (current implementation works, but standardization would improve maintainability) + +## Guidelines for New Components + +When creating a new DAK component: + +1. ✅ **Always use wrapper + content pattern** for components with PageLayout +2. ✅ **Call page hooks in content component** (after PageProvider exists) +3. ✅ **Handle loading and error states** in content component +4. ✅ **Export the wrapper component** (not the content component) +5. ✅ **Use `usePage()` hook** for accessing page context (preferred over `useDAKParams()`) +6. ✅ **Test with URL navigation** to ensure context is properly initialized + +## Testing Checklist + +For any new or modified component: + +- [ ] Navigate to component via URL: `/{component}/{user}/{repo}/{branch}` +- [ ] Check browser console for "PageContext is null" errors +- [ ] Verify component handles loading state gracefully +- [ ] Verify component handles error state (missing repo, etc.) +- [ ] Test branch switching functionality +- [ ] Verify all page hooks are called in content component + +## References + +- **Page Framework Documentation**: `public/docs/page-framework.md` (Component Architecture Patterns section) +- **Requirements Documentation**: `public/docs/requirements.md` (REQ-ARCH-001, REQ-ARCH-002, REQ-ARCH-003) +- **Issue Fix**: PR #1076 - ActorEditor page load failure fix +- **Reference Implementations**: DAKDashboard, ActorEditor, CoreDataDictionaryViewer + +## Migration Strategy + +For components that need updates: + +1. **Low Risk**: Components already working (like BusinessProcessSelection) + - Can be updated opportunistically during other maintenance + - Update documentation to note non-standard pattern + +2. **High Priority**: Any new components being developed + - Must follow wrapper + content pattern from the start + - Code review should verify pattern compliance + +3. **Code Review Checklist**: Add architecture pattern verification to PR reviews + - Verify wrapper + content pattern for PageLayout components + - Check that hooks are called after PageProvider + - Ensure proper loading/error handling + +## Future Considerations + +1. **Linter Rule**: Consider adding ESLint rule to detect page hooks called before PageLayout +2. **Component Generator**: Create a code generator/template for new DAK components +3. **Automated Testing**: Add integration tests that verify proper context initialization +4. **Documentation**: Keep this audit updated as components are added/modified diff --git a/public/docs/page-framework.md b/public/docs/page-framework.md index 2abf2defb7..c15207fe65 100644 --- a/public/docs/page-framework.md +++ b/public/docs/page-framework.md @@ -724,6 +724,317 @@ The page framework includes several built-in tutorials: For complete tutorial framework documentation, see [Tutorial Framework](tutorial-framework.md). +## Component Architecture Patterns + +### Wrapper + Content Pattern for PageLayout Components + +**CRITICAL REQUIREMENT**: All DAK components that use `PageLayout` MUST follow the wrapper + content component pattern to ensure proper PageProvider context initialization. + +#### The Problem + +When a component uses `PageLayout` and calls page context hooks (like `usePage()` or `useDAKParams()`) at the top level, it creates a chicken-and-egg problem: + +```javascript +// ❌ ANTI-PATTERN - DO NOT USE +const MyComponent = () => { + const { profile, repository } = usePage(); // ← ERROR: Context doesn't exist yet! + + return ( + // ← PageProvider created here +
Content
+
+ ); +}; +``` + +**Why this fails**: +1. React components execute from top to bottom +2. `usePage()` tries to access PageProvider context during component initialization +3. But `PageLayout` (which provides `PageProvider`) is in the return statement +4. The context doesn't exist when the hook is called +5. Result: `PageContext is null - component not wrapped in PageProvider` + +#### The Solution: Wrapper + Content Pattern + +Split your component into two parts: + +```javascript +// ✅ CORRECT PATTERN - ALWAYS USE THIS +const MyComponent = () => { + return ( + + + + ); +}; + +const MyComponentContent = () => { + const { profile, repository, branch, loading, error } = usePage(); + + // All component logic and state here + const [data, setData] = useState(null); + + useEffect(() => { + // Effects can safely use page context + if (profile && repository) { + // Load data... + } + }, [profile, repository]); + + return ( +
+ {/* Component UI */} +
+ ); +}; + +export default MyComponent; +``` + +**Why this works**: +1. `MyComponent` renders and returns `` +2. `PageLayout` renders and provides `PageProvider` +3. `MyComponentContent` starts rendering (now inside PageProvider) +4. `usePage()` is called, context exists ✅ + +### Required Pattern for Different Page Types + +#### DAK/Asset Pages (using PageLayout) + +**REQUIRED**: Use wrapper + content pattern + +```javascript +import { PageLayout, usePage } from './framework'; + +const DAKComponent = () => { + return ( + + + + ); +}; + +const DAKComponentContent = () => { + const { profile, repository, branch, loading, error } = usePage(); + + // Handle loading state + if (loading) { + return
Loading...
; + } + + // Handle error state + if (error) { + return
Error: {error}
; + } + + // Main content + return ( +
+

{repository?.name}

+ {/* Component implementation */} +
+ ); +}; + +export default DAKComponent; +``` + +#### Asset Editor Pages (using AssetEditorLayout) + +**NOT REQUIRED**: AssetEditorLayout handles context internally + +```javascript +import { AssetEditorLayout, usePage } from './framework'; + +const AssetEditor = () => { + // ✅ OK: AssetEditorLayout provides context internally + const { profile, repository, branch } = usePage(); + + return ( + + {/* Editor content */} + + ); +}; + +export default AssetEditor; +``` + +### Hook Usage Guidelines + +#### usePage() vs useDAKParams() + +**usePage()**: +- Returns page context regardless of page type +- More flexible, suitable for most components +- **RECOMMENDED** for components using wrapper + content pattern + +**useDAKParams()**: +- Validates page type is DAK or ASSET +- Returns error if used on wrong page type +- Less flexible but provides validation +- Can be used in content component after PageProvider exists + +```javascript +// ✅ Recommended approach +const MyComponentContent = () => { + const { profile, repository, branch } = usePage(); + // ... +}; + +// ✅ Also acceptable (if validation needed) +const MyComponentContent = () => { + const { profile, repository, branch } = useDAKParams(); + // Note: This will show warnings if used on non-DAK pages + // ... +}; +``` + +### Component Checklist + +Before deploying a new DAK component, verify: + +- [ ] Component uses wrapper + content pattern +- [ ] Wrapper component only wraps `PageLayout` (no hooks) +- [ ] Content component uses `usePage()` or `useDAKParams()` +- [ ] All hooks are called in content component (after PageProvider) +- [ ] Loading and error states are handled +- [ ] Component exports the wrapper (not content) + +### Examples from Codebase + +#### Correct Implementations ✅ + +1. **ActorEditor** (src/components/ActorEditor.js) +```javascript +const ActorEditor = () => { + return ( + + + + ); +}; + +const ActorEditorContent = () => { + const { profile, repository, branch } = usePage(); + // ... component logic +}; +``` + +2. **CoreDataDictionaryViewer** (src/components/CoreDataDictionaryViewer.js) +```javascript +const CoreDataDictionaryViewer = () => { + return ( + + + + ); +}; + +const CoreDataDictionaryViewerContent = () => { + const { profile, repository, branch } = usePage(); + // ... component logic +}; +``` + +3. **ComponentEditor** (src/components/ComponentEditor.js) +```javascript +const ComponentEditor = () => { + return ( + + + + ); +}; +``` + +4. **PersonaViewer** (src/components/PersonaViewer.js) +```javascript +const PersonaViewer = () => { + return ( + + + + ); +}; +``` + +5. **DAKDashboard** (src/components/DAKDashboard.js) +```javascript +const DAKDashboard = () => { + return ( + + + + ); +}; +``` + +#### Components Needing Attention ⚠️ + +The following components may need review or updates: + +1. **BusinessProcessSelection** - Uses `useDAKUrlParams` instead of page framework hooks +2. **DecisionSupportLogicView** - Uses `useDAKParams` in content (should use `usePage`) + +### Common Mistakes to Avoid + +1. **❌ Calling hooks before PageProvider** +```javascript +const MyComponent = () => { + const { profile } = usePage(); // ERROR! + return ...; +}; +``` + +2. **❌ Not handling loading/error states** +```javascript +const MyComponentContent = () => { + const { profile, repository } = usePage(); + // Missing: if (!profile || !repository) return loading... + return
{repository.name}
; // Can crash! +}; +``` + +3. **❌ Using useDAKParams without wrapper pattern** +```javascript +const MyComponent = () => { + const params = useDAKParams(); // ERROR! + return ...; +}; +``` + +4. **❌ Mixing patterns** +```javascript +const MyComponent = () => { + return ( + +
+ {/* Direct content instead of separate component */} +
+
+ ); +}; +// Missing: MyComponentContent that uses hooks +``` + +### Testing Your Component + +Verify your component works correctly: + +1. **URL Pattern Test**: Navigate to `/{component}/{user}/{repo}/{branch}` +2. **Console Check**: No "PageContext is null" errors +3. **Loading States**: Component handles loading gracefully +4. **Error States**: Component handles missing context +5. **Branch Switching**: Component updates when branch changes + ## Migration Guide To migrate existing pages to the enhanced framework: @@ -732,6 +1043,7 @@ To migrate existing pages to the enhanced framework: 2. **Replace authentication checks**: Use access services instead of direct GitHub checks 3. **Update save operations**: Use data access layer for consistent behavior 4. **Add user type handling**: Ensure pages work for all user types -5. **Test thoroughly**: Verify functionality across user types and permission levels +5. **Apply wrapper + content pattern**: Split components that use PageLayout and page hooks +6. **Test thoroughly**: Verify functionality across user types and permission levels The framework is designed to minimize changes to existing page logic while providing comprehensive user access management and consistent behavior across the application. \ No newline at end of file diff --git a/public/docs/requirements.md b/public/docs/requirements.md index 798b0075cf..9e52127e1e 100644 --- a/public/docs/requirements.md +++ b/public/docs/requirements.md @@ -368,6 +368,49 @@ For detailed information about each DAK component, see [DAK Components Documenta - CDN compatibility (Netlify, Vercel) - Specific GitHub Pages integration for smart-base repo +### 3.7 Component Architecture + +**REQ-ARCH-001**: The system SHALL enforce consistent component architecture patterns for PageLayout components +- All DAK components using `PageLayout` MUST follow the wrapper + content pattern +- Wrapper component MUST only wrap `PageLayout` with no hooks +- Content component MUST use page hooks (`usePage()` or `useDAKParams()`) to access context +- This pattern ensures `PageProvider` context exists before hooks attempt to access it + +**REQ-ARCH-002**: The system SHALL provide clear component structure requirements +- Component exports MUST be the wrapper component (not content) +- All React hooks MUST be called in the content component (after PageProvider initialization) +- Loading and error states MUST be handled in the content component +- Components MUST follow the established pattern from `DAKDashboard`, `ActorEditor`, and `CoreDataDictionaryViewer` + +**REQ-ARCH-003**: The system SHALL prevent PageProvider context initialization errors +- No page hooks (`usePage()`, `useDAKParams()`) SHALL be called before `PageProvider` exists +- Components SHALL handle the case where page context is loading or contains errors +- Components SHALL degrade gracefully when required context is unavailable + +**Component Pattern Example**: +```javascript +// Wrapper component - exports this +const MyDAKComponent = () => { + return ( + + + + ); +}; + +// Content component - contains all logic and hooks +const MyDAKComponentContent = () => { + const { profile, repository, branch, loading, error } = usePage(); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return
{/* Component implementation */}
; +}; + +export default MyDAKComponent; +``` + ## 4. User Experience Requirements ### 4.1 Branding diff --git a/src/components/ActorEditor.js b/src/components/ActorEditor.js index a614ba34ce..22171de17a 100644 --- a/src/components/ActorEditor.js +++ b/src/components/ActorEditor.js @@ -1,506 +1,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import actorDefinitionService from '../services/actorDefinitionService'; -import { PageLayout, useDAKParams } from './framework'; +import { PageLayout, usePage } from './framework'; -const ActorEditor = () => { - const navigate = useNavigate(); - const pageParams = useDAKParams(); - - // For now, we'll set editActorId to null since it's not in URL params - // This could be enhanced later to support URL-based actor editing - const editActorId = null; - - // State management - ALL HOOKS MUST BE AT THE TOP - const [actorDefinition, setActorDefinition] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [errors, setErrors] = useState({}); - const [showPreview, setShowPreview] = useState(false); - const [fshPreview, setFshPreview] = useState(''); - const [stagedActors, setStagedActors] = useState([]); - const [showActorList, setShowActorList] = useState(false); - const [activeTab, setActiveTab] = useState('basic'); - - // Initialize component - const initializeEditor = useCallback(async () => { - setLoading(true); - - try { - if (editActorId) { - // Load existing actor definition from staging ground - const actorData = actorDefinitionService.getFromStagingGround(editActorId); - if (actorData) { - setActorDefinition(actorData.actorDefinition); - } else { - setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); - } - } else { - // Create new actor definition - setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); - } - - // Load staged actors list - const staged = actorDefinitionService.listStagedActors(); - setStagedActors(staged); - - } catch (error) { - console.error('Error initializing actor editor:', error); - setErrors({ initialization: 'Failed to initialize editor' }); - } finally { - setLoading(false); - } - }, [editActorId]); - - useEffect(() => { - // Only initialize if we have valid page parameters - if (!pageParams.error && !pageParams.loading) { - initializeEditor(); - } - }, [pageParams.error, pageParams.loading, initializeEditor]); - - // Handle form field changes - const handleFieldChange = useCallback((field, value) => { - setActorDefinition(prev => ({ - ...prev, - [field]: value - })); - - // Clear field-specific errors - if (errors[field]) { - setErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[field]; - return newErrors; - }); - } - }, [errors]); - - // Handle nested field changes - const handleNestedFieldChange = useCallback((parentField, index, field, value) => { - setActorDefinition(prev => { - const newDefinition = { ...prev }; - if (!newDefinition[parentField]) { - newDefinition[parentField] = []; - } - if (!newDefinition[parentField][index]) { - newDefinition[parentField][index] = {}; - } - newDefinition[parentField][index][field] = value; - return newDefinition; - }); - }, []); - - // Add new item to array field - const addArrayItem = useCallback((field, defaultItem = {}) => { - setActorDefinition(prev => ({ - ...prev, - [field]: [...(prev[field] || []), defaultItem] - })); - }, []); - - // Remove item from array field - const removeArrayItem = useCallback((field, index) => { - setActorDefinition(prev => ({ - ...prev, - [field]: prev[field].filter((_, i) => i !== index) - })); - }, []); - - // Validate form data - const validateForm = useCallback(() => { - const newErrors = {}; - - if (!actorDefinition?.id) { - newErrors.id = 'Actor ID is required'; - } - - if (!actorDefinition?.name) { - newErrors.name = 'Actor name is required'; - } - - if (!actorDefinition?.description) { - newErrors.description = 'Actor description is required'; - } - - if (!actorDefinition?.type) { - newErrors.type = 'Actor type is required'; - } - - // Validate roles - if (actorDefinition?.roles) { - actorDefinition.roles.forEach((role, index) => { - if (!role.code) { - newErrors[`roles.${index}.code`] = 'Role code is required'; - } - if (!role.display) { - newErrors[`roles.${index}.display`] = 'Role display name is required'; - } - if (!role.system) { - newErrors[`roles.${index}.system`] = 'Role system is required'; - } - }); - } - - // Validate qualifications - if (actorDefinition?.qualifications) { - actorDefinition.qualifications.forEach((qual, index) => { - if (!qual.code) { - newErrors[`qualifications.${index}.code`] = 'Qualification code is required'; - } - if (!qual.display) { - newErrors[`qualifications.${index}.display`] = 'Qualification display name is required'; - } - }); - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }, [actorDefinition]); - - // Generate FSH preview - const generatePreview = useCallback(() => { - if (!actorDefinition) return; - - try { - const fsh = actorDefinitionService.generateFSH(actorDefinition); - setFshPreview(fsh); - } catch (error) { - console.error('Error generating FSH preview:', error); - setErrors({ general: 'Failed to generate FSH preview' }); - } - }, [actorDefinition]); - - // Save actor definition to staging ground - const handleSave = useCallback(async () => { - if (!validateForm()) { - return; - } - - setSaving(true); - - try { - actorDefinitionService.saveToStagingGround(actorDefinition, { - type: 'actor-definition', - actorId: actorDefinition.id, - actorName: actorDefinition.name, - branch: branch, - repository: repository?.name, - owner: profile?.login - }); - - // Refresh staged actors list - const staged = actorDefinitionService.listStagedActors(); - setStagedActors(staged); - - setErrors({}); - } catch (error) { - console.error('Error saving actor definition:', error); - setErrors({ general: 'Failed to save actor definition' }); - } finally { - setSaving(false); - } - }, [actorDefinition, validateForm, branch, repository, profile]); - - // Load template - const loadTemplate = useCallback((templateId) => { - const templates = actorDefinitionService.getActorTemplates(); - const template = templates.find(t => t.id === templateId); - if (template) { - setActorDefinition(template); - } - }, []); - - // Load staged actor - const loadStagedActor = useCallback((actorId) => { - const actorData = actorDefinitionService.getFromStagingGround(actorId); - if (actorData) { - setActorDefinition(actorData.actorDefinition); - setShowActorList(false); - } - }, []); - - // Delete staged actor - const deleteStagedActor = useCallback((actorId) => { - if (window.confirm('Are you sure you want to delete this staged actor?')) { - actorDefinitionService.removeFromStagingGround(actorId); - const staged = actorDefinitionService.listStagedActors(); - setStagedActors(staged); - } - }, []); - - // Handle PageProvider initialization issues - AFTER all hooks - if (pageParams.error) { - return ( - -
-
-

Page Context Error

-

{pageParams.error}

-

This component requires a DAK repository context to function properly.

-
-
-
- ); - } - - if (pageParams.loading) { - return ( - -
-
-

Loading...

-

Initializing page context...

-
-
-
- ); - } - - const { profile, repository, branch } = pageParams; - - return ( - -
- {!profile || !repository ? ( -
-

Redirecting...

-

Missing required context. Redirecting to home page...

-
- ) : loading ? ( -
-
-

Loading Actor Editor...

-

Initializing editor and loading data...

-
-
- ) : ( -
- -
-
- - -
-
- -
-
- - {errors.general && ( -
- Error: {errors.general} -
- )} - -
- {/* Staged Actors Sidebar */} - {showActorList && ( -
-
-

Staged Actors

- -
-
-
-

Templates

- {actorDefinitionService.getActorTemplates().map(template => ( -
- {template.name} - -
- ))} -
- - {stagedActors.length > 0 && ( -
-

Staged Actors

- {stagedActors.map(actor => ( -
-
- {actor.name} - {actor.id} - - {new Date(actor.lastModified).toLocaleDateString()} - -
-
- - -
-
- ))} -
- )} -
-
- )} - - {/* Main Editor */} -
- {actorDefinition && ( - <> -
- - - - -
- -
- {activeTab === 'basic' && ( - - )} - - {activeTab === 'roles' && ( - - )} - - {activeTab === 'context' && ( - - )} - - {activeTab === 'metadata' && ( - - )} -
- - )} -
-
-
- )} - - {/* FSH Preview Modal */} - {showPreview && ( -
setShowPreview(false)} - role="presentation" - > -
e.stopPropagation()} - role="dialog" - aria-labelledby="fsh-preview-title" - aria-modal="true" - > -
-

FSH Preview

- -
-
-
{fshPreview}
-
-
- -
-
-
- )} -
-
- ); -}; - -// Basic Info Tab Component +// Basic Info Tab Component - MUST BE DEFINED BEFORE ActorEditor const BasicInfoTab = ({ actorDefinition, errors, onFieldChange }) => (

Basic Information

@@ -567,7 +70,7 @@ const BasicInfoTab = ({ actorDefinition, errors, onFieldChange }) => (
); -// Roles Tab Component +// Roles Tab Component - MUST BE DEFINED BEFORE ActorEditor const RolesTab = ({ actorDefinition, errors, onNestedFieldChange, onAddItem, onRemoveItem }) => (

Roles & Qualifications

@@ -599,35 +102,41 @@ const RolesTab = ({ actorDefinition, errors, onNestedFieldChange, onAddItem, onR
- + onNestedFieldChange('roles', index, 'code', e.target.value)} + className={errors[`roles.${index}.code`] ? 'error' : ''} placeholder="Role code" /> + {errors[`roles.${index}.code`] && {errors[`roles.${index}.code`]}}
- + onNestedFieldChange('roles', index, 'display', e.target.value)} + className={errors[`roles.${index}.display`] ? 'error' : ''} placeholder="Human-readable role name" /> + {errors[`roles.${index}.display`] && {errors[`roles.${index}.display`]}}
- + onNestedFieldChange('roles', index, 'system', e.target.value)} + className={errors[`roles.${index}.system`] ? 'error' : ''} placeholder="http://snomed.info/sct" /> + {errors[`roles.${index}.system`] && {errors[`roles.${index}.system`]}}
))} @@ -659,24 +168,28 @@ const RolesTab = ({ actorDefinition, errors, onNestedFieldChange, onAddItem, onR
- + onNestedFieldChange('qualifications', index, 'code', e.target.value)} + className={errors[`qualifications.${index}.code`] ? 'error' : ''} placeholder="Qualification code" /> + {errors[`qualifications.${index}.code`] && {errors[`qualifications.${index}.code`]}}
- + onNestedFieldChange('qualifications', index, 'display', e.target.value)} + className={errors[`qualifications.${index}.display`] ? 'error' : ''} placeholder="Qualification name" /> + {errors[`qualifications.${index}.display`] && {errors[`qualifications.${index}.display`]}}
@@ -695,23 +208,242 @@ const RolesTab = ({ actorDefinition, errors, onNestedFieldChange, onAddItem, onR
-

Specialties

+

Specialties

+ +
+ + {actorDefinition.specialties && actorDefinition.specialties.map((specialty, index) => ( +
+
+ Specialty {index + 1} + +
+
+
+ + onNestedFieldChange('specialties', index, 'code', e.target.value)} + placeholder="Specialty code" + /> +
+
+ + onNestedFieldChange('specialties', index, 'display', e.target.value)} + placeholder="Specialty name" + /> +
+
+
+ + onNestedFieldChange('specialties', index, 'system', e.target.value)} + placeholder="http://snomed.info/sct" + /> +
+
+ ))} +
+
+); + +// Context Tab Component - MUST BE DEFINED BEFORE ActorEditor +const ContextTab = ({ actorDefinition, errors, onFieldChange, onNestedFieldChange, onAddItem, onRemoveItem }) => ( +
+

Context & Access

+ +
+

Typical Location

+
+
+ + +
+
+ + onFieldChange('location', { ...actorDefinition.location, description: e.target.value })} + placeholder="Describe the typical location" + /> +
+
+
+ +
+

System Access Level

+
+ +
+
+ +
+
+

Key Interactions

+ +
+ + {actorDefinition.interactions && actorDefinition.interactions.map((interaction, index) => ( +
+
+ Interaction {index + 1} + +
+
+
+ + +
+
+ + onNestedFieldChange('interactions', index, 'target', e.target.value)} + placeholder="What the actor interacts with" + /> +
+
+
+ + onNestedFieldChange('interactions', index, 'description', e.target.value)} + placeholder="Describe this interaction" + /> +
+
+ ))} +
+
+); + +// Metadata Tab Component - MUST BE DEFINED BEFORE ActorEditor +const MetadataTab = ({ actorDefinition, errors, onFieldChange, onNestedFieldChange, onAddItem, onRemoveItem }) => ( +
+

Metadata

+ +
+
+ + onFieldChange('metadata', { ...actorDefinition.metadata, version: e.target.value })} + placeholder="1.0.0" + /> +
+
+ + +
+
+ +
+ + onFieldChange('metadata', { ...actorDefinition.metadata, publisher: e.target.value })} + placeholder="Organization or person responsible" + /> +
+ +
+
+

Contact Information

- {actorDefinition.specialties && actorDefinition.specialties.map((specialty, index) => ( + {actorDefinition.metadata?.contact && actorDefinition.metadata.contact.map((contact, index) => (
- Specialty {index + 1} + Contact {index + 1}
- + onNestedFieldChange('specialties', index, 'code', e.target.value)} - placeholder="Specialty code" + value={contact.name || ''} + onChange={(e) => { + const newContacts = [...(actorDefinition.metadata.contact || [])]; + newContacts[index] = { ...contact, name: e.target.value }; + onFieldChange('metadata', { ...actorDefinition.metadata, contact: newContacts }); + }} + placeholder="Contact name" />
- + onNestedFieldChange('specialties', index, 'display', e.target.value)} - placeholder="Specialty name" + type="email" + value={contact.email || ''} + onChange={(e) => { + const newContacts = [...(actorDefinition.metadata.contact || [])]; + newContacts[index] = { ...contact, email: e.target.value }; + onFieldChange('metadata', { ...actorDefinition.metadata, contact: newContacts }); + }} + placeholder="contact@example.com" />
-
- - onNestedFieldChange('specialties', index, 'system', e.target.value)} - placeholder="http://snomed.info/sct" - /> -
))}
+ +
+ + onFieldChange('metadata', { + ...actorDefinition.metadata, + tags: e.target.value.split(',').map(tag => tag.trim()).filter(tag => tag) + })} + placeholder="Enter tags separated by commas" + /> + Comma-separated tags for categorization +
); -// Context Tab Component -const ContextTab = ({ actorDefinition, errors, onFieldChange, onNestedFieldChange, onAddItem, onRemoveItem }) => ( -
-

Context & Access

+const ActorEditor = () => { + return ( + + + + ); +}; + +const ActorEditorContent = () => { + const navigate = useNavigate(); + const { profile, repository, branch, loading: pageLoading, error: pageError } = usePage(); + + // For now, we'll set editActorId to null since it's not in URL params + // This could be enhanced later to support URL-based actor editing + const editActorId = null; + + // State management - ALL HOOKS MUST BE AT THE TOP + const [actorDefinition, setActorDefinition] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [errors, setErrors] = useState({}); + const [showPreview, setShowPreview] = useState(false); + const [fshPreview, setFshPreview] = useState(''); + const [stagedActors, setStagedActors] = useState([]); + const [showActorList, setShowActorList] = useState(false); + const [activeTab, setActiveTab] = useState('basic'); + const [successMessage, setSuccessMessage] = useState(''); + + // Initialize component + const initializeEditor = useCallback(async () => { + setLoading(true); + + try { + if (editActorId) { + // Load existing actor definition from staging ground + const actorData = actorDefinitionService.getFromStagingGround(editActorId); + if (actorData) { + setActorDefinition(actorData.actorDefinition); + } else { + setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); + } + } else { + // Create new actor definition + setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); + } + + // Load staged actors list + const staged = actorDefinitionService.listStagedActors(); + setStagedActors(staged); + + } catch (error) { + console.error('Error initializing actor editor:', error); + setErrors({ initialization: 'Failed to initialize editor' }); + } finally { + setLoading(false); + } + }, [editActorId]); + + useEffect(() => { + // Initialize editor once page context is loaded (even without DAK context) + // ActorEditor can work standalone to save to staging ground + if (!pageLoading && !pageError) { + initializeEditor(); + } + }, [pageLoading, pageError, initializeEditor]); + + // Handle form field changes + const handleFieldChange = useCallback((field, value) => { + setActorDefinition(prev => ({ + ...prev, + [field]: value + })); + + // Clear field-specific errors + if (errors[field]) { + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[field]; + return newErrors; + }); + } + }, [errors]); + + // Handle nested field changes + const handleNestedFieldChange = useCallback((parentField, index, field, value) => { + setActorDefinition(prev => { + const newDefinition = { ...prev }; + if (!newDefinition[parentField]) { + newDefinition[parentField] = []; + } + if (!newDefinition[parentField][index]) { + newDefinition[parentField][index] = {}; + } + newDefinition[parentField][index][field] = value; + return newDefinition; + }); + + // Clear field-specific errors for nested fields + const errorKey = `${parentField}.${index}.${field}`; + if (errors[errorKey]) { + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[errorKey]; + return newErrors; + }); + } + }, [errors]); + + // Add new item to array field + const addArrayItem = useCallback((field, defaultItem = {}) => { + setActorDefinition(prev => ({ + ...prev, + [field]: [...(prev[field] || []), defaultItem] + })); + }, []); + + // Remove item from array field + const removeArrayItem = useCallback((field, index) => { + setActorDefinition(prev => ({ + ...prev, + [field]: prev[field].filter((_, i) => i !== index) + })); + }, []); + + // Validate form data + const validateForm = useCallback(() => { + const newErrors = {}; + + if (!actorDefinition?.id) { + newErrors.id = 'Actor ID is required'; + } + + if (!actorDefinition?.name) { + newErrors.name = 'Actor name is required'; + } + + if (!actorDefinition?.description) { + newErrors.description = 'Actor description is required'; + } + + if (!actorDefinition?.type) { + newErrors.type = 'Actor type is required'; + } + + // Validate roles + if (actorDefinition?.roles) { + actorDefinition.roles.forEach((role, index) => { + if (!role.code) { + newErrors[`roles.${index}.code`] = 'Role code is required'; + } + if (!role.display) { + newErrors[`roles.${index}.display`] = 'Role display name is required'; + } + if (!role.system) { + newErrors[`roles.${index}.system`] = 'Role system is required'; + } + }); + } + + // Validate qualifications + if (actorDefinition?.qualifications) { + actorDefinition.qualifications.forEach((qual, index) => { + if (!qual.code) { + newErrors[`qualifications.${index}.code`] = 'Qualification code is required'; + } + if (!qual.display) { + newErrors[`qualifications.${index}.display`] = 'Qualification display name is required'; + } + }); + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, [actorDefinition]); + + // Generate FSH preview + const generatePreview = useCallback(() => { + if (!actorDefinition) return; -
-

Typical Location

-
-
- - + try { + const fsh = actorDefinitionService.generateFSH(actorDefinition); + setFshPreview(fsh); + setShowPreview(true); + } catch (error) { + console.error('Error generating FSH preview:', error); + setErrors({ general: 'Failed to generate FSH preview: ' + error.message }); + } + }, [actorDefinition]); + + // Save actor definition to staging ground + const handleSave = useCallback(async () => { + console.log('handleSave called with actorDefinition:', actorDefinition); + + const isValid = validateForm(); + console.log('Form validation result:', isValid, 'Errors:', errors); + + if (!isValid) { + console.log('Form validation failed - showing errors'); + + // Build a user-friendly error message listing all validation issues + const errorFields = Object.keys(errors).filter(key => key !== 'general'); + const errorCount = errorFields.length; + + let errorMessage = `Please fix ${errorCount} validation error${errorCount !== 1 ? 's' : ''} before saving:\n`; + + // Group errors by type for better readability + const basicErrors = errorFields.filter(key => !key.includes('.')); + const nestedErrors = errorFields.filter(key => key.includes('.')); + + if (basicErrors.length > 0) { + errorMessage += '\n• Basic fields: ' + basicErrors.map(key => { + const label = key.charAt(0).toUpperCase() + key.slice(1); + return label; + }).join(', '); + } + + if (nestedErrors.length > 0) { + const roleErrors = nestedErrors.filter(key => key.startsWith('roles.')); + const qualErrors = nestedErrors.filter(key => key.startsWith('qualifications.')); + + if (roleErrors.length > 0) { + errorMessage += '\n• Role fields are missing or invalid (check the Roles tab)'; + } + if (qualErrors.length > 0) { + errorMessage += '\n• Qualification fields are missing or invalid (check the Roles tab)'; + } + } + + errorMessage += '\n\nLook for red highlighted fields and error messages below each field.'; + + setErrors(prev => ({ + ...prev, + general: errorMessage + })); + return; + } + + setSaving(true); + setErrors({}); + setSuccessMessage(''); + + try { + console.log('Calling saveToStagingGround...'); + const result = await actorDefinitionService.saveToStagingGround(actorDefinition); + console.log('Save result:', result); + + if (result.success) { + // Refresh staged actors list + const staged = actorDefinitionService.listStagedActors(); + console.log('Staged actors after save:', staged); + setStagedActors(staged); + + // Show success message + setSuccessMessage(`✅ Saved "${actorDefinition.name}" to staging ground`); + + // Clear success message after 3 seconds + setTimeout(() => setSuccessMessage(''), 3000); + } else { + console.error('Save failed:', result.error); + setErrors({ general: result.error || 'Failed to save actor definition' }); + } + } catch (error) { + console.error('Error saving actor definition:', error); + setErrors({ general: 'Failed to save actor definition: ' + error.message }); + } finally { + setSaving(false); + } + }, [actorDefinition, validateForm, errors]); + + // Load template + const loadTemplate = useCallback((templateId) => { + const templates = actorDefinitionService.getActorTemplates(); + const template = templates.find(t => t.id === templateId); + if (template) { + setActorDefinition(template); + } + }, []); + + // Load staged actor + const loadStagedActor = useCallback((actorId) => { + const actorData = actorDefinitionService.getFromStagingGround(actorId); + if (actorData) { + setActorDefinition(actorData.actorDefinition); + setShowActorList(false); + } + }, []); + + // Delete staged actor + const deleteStagedActor = useCallback((actorId) => { + if (window.confirm('Are you sure you want to delete this staged actor?')) { + actorDefinitionService.removeFromStagingGround(actorId); + const staged = actorDefinitionService.listStagedActors(); + setStagedActors(staged); + } + }, []); + + // Handle page loading and errors + if (pageError) { + return ( +
+
+

Page Context Error

+

{pageError}

+

This component requires a DAK repository context to function properly.

-
- - onFieldChange('location', { ...actorDefinition.location, description: e.target.value })} - placeholder="Describe the typical location" - /> +
+ ); + } + + if (pageLoading) { + return ( +
+
+

Loading...

+

Initializing page context...

-
+ ); + } -
-

System Access Level

-
- -
-
+ return ( +
+ {loading ? ( +
+
+

Loading Actor Editor...

+

Initializing editor and loading data...

+
+
+ ) : ( +
-
-
-

Key Interactions

- -
- - {actorDefinition.interactions && actorDefinition.interactions.map((interaction, index) => ( -
-
- Interaction {index + 1} +
+
-
-
-
- - -
-
- - onNestedFieldChange('interactions', index, 'target', e.target.value)} - placeholder="What the actor interacts with" - /> -
-
-
- - onNestedFieldChange('interactions', index, 'description', e.target.value)} - placeholder="Describe this interaction" - /> -
-
- ))} -
-
-); - -// Metadata Tab Component -const MetadataTab = ({ actorDefinition, errors, onFieldChange, onNestedFieldChange, onAddItem, onRemoveItem }) => ( -
-

Metadata

- -
-
- - onFieldChange('metadata', { ...actorDefinition.metadata, version: e.target.value })} - placeholder="1.0.0" - /> -
-
- - -
-
- -
- - onFieldChange('metadata', { ...actorDefinition.metadata, publisher: e.target.value })} - placeholder="Organization or person responsible" - /> -
- -
-
-

Contact Information

- -
- - {actorDefinition.metadata?.contact && actorDefinition.metadata.contact.map((contact, index) => ( -
-
- Contact {index + 1} +
+
+
-
-
- - { - const newContacts = [...(actorDefinition.metadata.contact || [])]; - newContacts[index] = { ...contact, name: e.target.value }; - onFieldChange('metadata', { ...actorDefinition.metadata, contact: newContacts }); - }} - placeholder="Contact name" - /> -
-
- - { - const newContacts = [...(actorDefinition.metadata.contact || [])]; - newContacts[index] = { ...contact, email: e.target.value }; - onFieldChange('metadata', { ...actorDefinition.metadata, contact: newContacts }); - }} - placeholder="contact@example.com" - /> +
+ + {successMessage && ( +
+ {successMessage} +
+ )} + + {errors.general && ( +
+ Error: {errors.general} +
+ )} + +
+ {/* Staged Actors Sidebar */} + {showActorList && ( +
+
+

Staged Actors

+ +
+
+
+

Templates

+ {actorDefinitionService.getActorTemplates().map(template => ( +
+ {template.name} + +
+ ))} +
+ + {stagedActors.length > 0 && ( +
+

Staged Actors

+ {stagedActors.map(actor => ( +
+
+ {actor.name} + {actor.id} + + {new Date(actor.lastModified).toLocaleDateString()} + +
+
+ + +
+
+ ))} +
+ )} +
+ )} + + {/* Main Editor */} +
+ {actorDefinition && ( + <> +
+ + + + +
+ +
+ {activeTab === 'basic' && ( + + )} + + {activeTab === 'roles' && ( + + )} + + {activeTab === 'context' && ( + + )} + + {activeTab === 'metadata' && ( + + )} +
+ + )}
- ))} -
+
+ )} -
- - onFieldChange('metadata', { - ...actorDefinition.metadata, - tags: e.target.value.split(',').map(tag => tag.trim()).filter(tag => tag) - })} - placeholder="Enter tags separated by commas" - /> - Comma-separated tags for categorization -
-
-); + {/* FSH Preview Modal */} + {showPreview && ( +
setShowPreview(false)} + role="presentation" + > +
e.stopPropagation()} + role="dialog" + aria-labelledby="fsh-preview-title" + aria-modal="true" + > +
+

FSH Preview

+ +
+
+
{fshPreview}
+
+
+ +
+
+
+ )} +
+ ); +}; export default ActorEditor; \ No newline at end of file