diff --git a/docs/FRAMEWORK_HOOKS_USAGE_GUIDE.md b/docs/FRAMEWORK_HOOKS_USAGE_GUIDE.md new file mode 100644 index 000000000..1f5071221 --- /dev/null +++ b/docs/FRAMEWORK_HOOKS_USAGE_GUIDE.md @@ -0,0 +1,310 @@ +# SGEX Framework Hooks Usage Guide + +## Overview + +This guide provides comprehensive instructions for using SGEX framework hooks correctly to avoid common deployment errors and ensure robust component behavior. + +## Page Framework Hooks + +### 1. `usePage()` - For DAK Viewer Components + +**Use Case**: Components that display/view DAK content in read-only mode +**Examples**: PersonaViewer, CoreDataDictionaryViewer, TestingViewer + +```javascript +import { PageLayout, usePage } from './framework'; + +const YourViewerComponent = () => { + return ( + + + + ); +}; + +const YourViewerContent = () => { + const pageContext = usePage(); + + // ALWAYS handle null context for deployment robustness + if (!pageContext) { + return ( +
+
+

Loading

+

Initializing page context...

+
+
+ ); + } + + // Handle unsuitable page context types + if (pageContext.type === 'top-level' || pageContext.type === 'unknown') { + return ( +
+
+

Repository Context Required

+

This component requires a DAK repository context.

+ /your-viewer/:user/:repo/:branch +
+
+ ); + } + + const { profile, repository, branch } = pageContext; + + // Your component logic here... +}; +``` + +### 2. `useDAKParams()` - For DAK Editor/Asset Components + +**Use Case**: Components that edit/modify DAK content and require strict DAK context +**Examples**: ActorEditor, QuestionnaireEditor, AssetEditor components + +```javascript +import { PageLayout, useDAKParams } from './framework'; + +const YourEditorComponent = () => { + const pageParams = useDAKParams(); + + // ALWAYS handle error state first + if (pageParams.error) { + return ( + +
+
+

Page Context Error

+

{pageParams.error}

+

This component requires a DAK repository context to function properly.

+
+
+
+ ); + } + + // Handle loading state + if (pageParams.loading) { + return ( + +
+
+

Loading...

+

Initializing page context...

+
+
+
+ ); + } + + const { profile, repository, branch } = pageParams; + + // Your editor logic here... +}; +``` + +### 3. `useUserParams()` - For User-Level Components + +**Use Case**: Components that operate at user/organization level +**Examples**: UserDashboard, OrganizationSettings + +```javascript +import { useUserParams } from './framework'; + +const YourUserComponent = () => { + const { user, profile, loading, error } = useUserParams(); + + if (loading) return
Loading user context...
; + if (error) return
Error: {error}
; + + // Your user-level logic here... +}; +``` + +## Hook Selection Decision Tree + +``` +Does your component need to: +├── Edit/modify DAK content? +│ └── Use useDAKParams() with error/loading handling +├── View/display DAK content? +│ └── Use usePage() with null/type checking +├── Work with user/org data? +│ └── Use useUserParams() +└── Access raw page parameters? + └── Use usePageParams() (advanced use only) +``` + +## Error Handling Patterns + +### Pattern 1: Graceful Degradation (Recommended for Viewers) + +```javascript +const { profile, repository, branch } = usePage(); + +// Show limited functionality if context is missing +if (!profile || !repository) { + return ( +
+

Repository Information Not Available

+

Some features may be limited without repository context.

+ {/* Show what you can without full context */} +
+ ); +} +``` + +### Pattern 2: Strict Requirements (Recommended for Editors) + +```javascript +const pageParams = useDAKParams(); + +if (pageParams.error || !pageParams.repository) { + return ( +
+

Repository Context Required

+

This editor requires full DAK repository access.

+
+ ); +} +``` + +## Common Deployment Issues and Solutions + +### Issue 1: `useDAKParams can only be used on DAK or Asset pages` + +**Cause**: Component using `useDAKParams()` loaded in wrong context +**Solution**: Use the defensive pattern above with error handling + +### Issue 2: `PageContext is null - component not wrapped in PageProvider` + +**Cause**: PageProvider not initialized in deployment environment +**Solution**: Always check for null context in components + +### Issue 3: Component crashes on direct URL access + +**Cause**: Missing defensive handling for various page states +**Solution**: Implement complete error/loading/null checking + +## Framework Architecture Compliance Checklist + +### For DAK Viewer Components (`usePage`) +- [ ] Uses `PageLayout` wrapper with correct `pageName` +- [ ] Handles null `pageContext` +- [ ] Handles unsuitable page types (`top-level`, `unknown`) +- [ ] Provides user feedback for missing context +- [ ] Uses `githubService` for API calls +- [ ] Follows standard URL pattern: `/:component/:user/:repo/:branch` + +### For DAK Editor Components (`useDAKParams`) +- [ ] Uses `PageLayout` or `AssetEditorLayout` wrapper +- [ ] Handles `pageParams.error` state first +- [ ] Handles `pageParams.loading` state +- [ ] Provides clear error messages for context issues +- [ ] Uses proper save/commit workflows +- [ ] Implements proper authentication checks + +### General Requirements +- [ ] Component is lazy-loaded in `componentRouteService.js` +- [ ] Route is configured in `routes-config.json` +- [ ] Component follows WHO branding guidelines +- [ ] Includes contextual help topics in `helpContentService.js` +- [ ] Has proper CSS classes and responsive design + +## Testing Your Components + +### Manual Testing Checklist +1. **Direct URL Access**: Navigate directly to component URL +2. **Unauthenticated Access**: Test without GitHub authentication +3. **Invalid Repository**: Test with non-existent repository +4. **Page Refresh**: Refresh page while on component +5. **Network Issues**: Test with intermittent connectivity + +### Deployment Testing +1. **Feature Branch Deployment**: Test on deployed feature branch +2. **Cache Issues**: Test after deployment cache clears +3. **Different Browsers**: Test cross-browser compatibility +4. **Mobile Devices**: Test responsive behavior + +## Framework Hooks Reference + +### Return Values + +#### `usePage()` Returns: +```javascript +{ + pageName: string, + user: string | null, + profile: object | null, + repository: object | null, + branch: string | null, + asset: string | null, + type: 'top-level' | 'user' | 'dak' | 'asset', + loading: boolean, + error: string | null, + isAuthenticated: boolean, + navigate: function, + params: object, + location: object +} +``` + +#### `useDAKParams()` Returns: +```javascript +{ + user: string | null, + profile: object | null, + repository: object | null, + branch: string | null, + asset: string | null, + updateBranch: function, + navigate: function, + loading: boolean, + error: string | null // NEW: Error message for graceful degradation +} +``` + +## Migration Guide + +### Migrating from Throwing `useDAKParams()` to Graceful Version + +**Old Pattern (Error-Prone):** +```javascript +const { profile, repository } = useDAKParams(); // Could throw error +``` + +**New Pattern (Robust):** +```javascript +const pageParams = useDAKParams(); + +if (pageParams.error) { + return ; +} + +if (pageParams.loading) { + return ; +} + +const { profile, repository } = pageParams; +``` + +## Best Practices Summary + +1. **Always handle error states first** before accessing data +2. **Provide meaningful error messages** to users +3. **Use loading states** for better UX during initialization +4. **Choose the right hook** for your component's purpose +5. **Test in deployment environments** not just local development +6. **Follow the component architecture patterns** consistently +7. **Include proper error boundaries** around framework hook usage + +## Support and Troubleshooting + +If you encounter issues with framework hooks: + +1. Check this guide for the correct usage pattern +2. Verify your component follows the architecture checklist +3. Test your component with direct URL access +4. Review browser console for specific error messages +5. Ensure PageProvider is properly configured in your app structure + +For deployment-specific issues, ensure your deployment correctly serves the static files and has proper routing configuration. \ No newline at end of file diff --git a/docs/FUTURE_ENHANCEMENTS.md b/docs/FUTURE_ENHANCEMENTS.md new file mode 100644 index 000000000..77b2668b6 --- /dev/null +++ b/docs/FUTURE_ENHANCEMENTS.md @@ -0,0 +1,149 @@ +# PersonaViewer Future Enhancements + +## Overview +This document outlines planned enhancements to the PersonaViewer component for creating, editing, and managing Generic Personas (ActorDefinitions) in DAK repositories. + +## Current Status +The PersonaViewer component currently provides: +- ✅ Scanning and viewing ActorDefinitions from FSH files (`input/fsh/actors`) +- ✅ Scanning and viewing ActorDefinitions from JSON files (`inputs/resources`) +- ✅ Display of found actors with links to source files +- ✅ Read-only viewing functionality + +## Planned Enhancements + +### 1. Create New Generic Persona +**Objective**: Allow users to create new Generic Personas via ActorDefinition FSH files + +**Requirements**: +- Follow the authoritative data model: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/fsh/models/GenericPersona.fsh +- Support all fields defined in GenericPersona.fsh: + - identifier (required) + - name/title + - description + - type (e.g., practitioner, patient, relatedperson) + - qualification + - communication + - telecom + - address + +**Implementation Approach**: +- Leverage existing ActorEditor component patterns for FSH file editing +- Integrate with actorDefinitionService for FSH generation +- Use staging ground for temporary storage before commit + +### 2. Edit Existing Generic Personas +**Objective**: Enable in-place editing of ActorDefinition FSH files + +**Requirements**: +- Load existing FSH file content +- Parse FSH to editable form fields +- Generate updated FSH on save +- Preserve FSH formatting and comments + +**Implementation Approach**: +- Extend actorDefinitionService.parseFSH() for comprehensive parsing +- Enhance actorDefinitionService.generateFSH() for all GenericPersona fields +- Add form validation based on FHIR ActorDefinition constraints + +### 3. Support for Requirements Models +**Objective**: Extend support to all WHO smart-base data models + +**Data Models to Support**: +- GenericPersona.fsh (priority 1) +- FunctionalRequirement.fsh +- NonFunctionalRequirement.fsh +- UserScenario.fsh +- Other models in https://github.com/WorldHealthOrganization/smart-base/tree/main/input/fsh/models + +**Requirements**: +- View/Edit/Create functionality for each model type +- Model-specific validation +- Consistent UI patterns across all model types + +### 4. Integration with ActorEditor +**Objective**: Consolidate persona management into unified workflow + +**Approach**: +- PersonaViewer: Read-only scanning and discovery +- ActorEditor: Full CRUD operations on ActorDefinitions +- Seamless navigation between viewer and editor modes +- Consistent data model and FSH handling + +## Technical Considerations + +### FSH File Handling +- Use actorDefinitionService for FSH parsing and generation +- Maintain compatibility with SUSHI FSH compiler +- Support FSH syntax highlighting and validation + +### GitHub Integration +- Use githubService for file operations +- Support branch-based workflows +- Implement proper commit messages and PR creation + +### Data Validation +- Validate against FHIR ActorDefinition specification +- Enforce WHO smart-base data model constraints +- Provide helpful error messages and guidance + +### User Experience +- Clear distinction between view and edit modes +- Intuitive form layouts matching FSH structure +- Real-time FSH preview +- Template selection for common persona types + +## Implementation Phases + +### Phase 1: Fix Current Build Issues +- ✅ Resolve React hooks violations in PersonaViewer +- ✅ Ensure build passes without errors +- ✅ Complete current PR for issue #996 + +### Phase 2: Basic Create Functionality +- Add "Create New Persona" button to PersonaViewer +- Implement basic form for GenericPersona fields +- Generate FSH file and save to staging ground +- Enable commit to repository + +### Phase 3: Edit Functionality +- Add "Edit" action to each displayed persona +- Load FSH file content into editable form +- Support save and update operations +- Handle FSH parsing edge cases + +### Phase 4: Advanced Features +- Template library for common persona types +- Bulk operations (import/export multiple personas) +- Version comparison and merge support +- Integration with other DAK components + +## Related Resources + +### Authoritative Data Models +- GenericPersona: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/fsh/models/GenericPersona.fsh +- All models: https://github.com/WorldHealthOrganization/smart-base/tree/main/input/fsh/models + +### Existing Code to Leverage +- `src/components/ActorEditor.js` - FSH editing patterns +- `src/services/actorDefinitionService.js` - FSH parsing and generation +- `src/services/githubService.js` - Repository operations +- `src/services/stagingGroundService.js` - Temporary file storage + +### Documentation +- `docs/FRAMEWORK_HOOKS_USAGE_GUIDE.md` - Framework hook usage patterns +- `public/docs/dak-components.md` - DAK component specifications +- `public/docs/solution-architecture.md` - Overall architecture + +## Success Criteria +- Users can create new Generic Personas without writing FSH code +- Existing personas can be edited with visual forms +- Generated FSH files comply with WHO smart-base standards +- Changes integrate smoothly with GitHub workflow +- UI follows SGEX component architecture patterns + +## Next Steps +1. Create new GitHub issue for PersonaViewer enhancements +2. Design detailed UI mockups for create/edit forms +3. Implement Phase 2 (Basic Create Functionality) +4. Iterate based on user feedback diff --git a/docs/PERSONA_VIEWER_ENHANCEMENTS.md b/docs/PERSONA_VIEWER_ENHANCEMENTS.md new file mode 100644 index 000000000..59e08e93c --- /dev/null +++ b/docs/PERSONA_VIEWER_ENHANCEMENTS.md @@ -0,0 +1,159 @@ +# PersonaViewer Future Enhancements + +## Overview +This document outlines planned enhancements for the PersonaViewer component to support creating, editing, and managing Generic Personas (ActorDefinitions) following WHO SMART Guidelines data models. + +## Current Status (Issue #996 - COMPLETED) +✅ PersonaViewer component created and functional +✅ Scans FSH files under `input/fsh/actors` for ActorDefinitions +✅ Scans JSON files under `inputs/resources` for ActorDefinition resources +✅ Displays found personas with links to source files +✅ Proper routing configuration (`/persona-viewer/:user/:repo/:branch`) +✅ Framework compliance and deployment robustness + +## Planned Enhancements + +### 1. Create New Generic Personas +**Objective**: Allow users to create new Generic Personas via ActorDefinition FSH files + +**Data Model**: Follow authoritative WHO smart-base model +- Reference: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/fsh/models/GenericPersona.fsh + +**Fields to Support**: +- Profile metadata (id, title, description) +- Actor type (person, organization, system) +- Roles and responsibilities +- Qualifications and certifications +- Specialties +- Location constraints +- Access levels +- Interactions with other actors +- Associated processes + +**Implementation Approach**: +- Leverage existing ActorEditor FSH editing capabilities +- Add "Create New Persona" button to PersonaViewer +- FSH template generation based on GenericPersona.fsh model +- Form-based editor with preview of generated FSH + +### 2. Edit Existing Personas +**Objective**: Enable in-place editing of ActorDefinition FSH files + +**Features**: +- Click-to-edit from PersonaViewer list +- Syntax highlighting for FSH content +- Validation against GenericPersona profile +- Save changes to staging ground +- Commit to repository workflow + +**Integration**: +- Use ActorEditor component for editing logic +- Share FSH generation/parsing utilities +- Maintain consistency with existing actor definition service + +### 3. Requirements Modeling Support +**Objective**: Follow WHO smart-base data models for functional/non-functional requirements + +**Data Models**: +- Reference: https://github.com/WorldHealthOrganization/smart-base/tree/main/input/fsh/models +- GenericPersona.fsh +- FunctionalRequirement.fsh +- NonFunctionalRequirement.fsh +- Other relevant models + +**Features**: +- Validate personas against requirements models +- Link personas to functional requirements +- Support requirement traceability +- Generate requirement documentation + +### 4. Enhanced Viewing Features +**Objective**: Improve persona visualization and navigation + +**Features**: +- Detailed persona profile viewer +- Relationship graph showing persona interactions +- Role hierarchy visualization +- Search and filter capabilities +- Export to various formats (JSON, FSH, documentation) + +### 5. Collaboration Features +**Objective**: Support team collaboration on persona definitions + +**Features**: +- Version history for persona changes +- Comments and annotations +- Review and approval workflow +- Merge conflict resolution for concurrent edits + +## Technical Considerations + +### React Hooks Compliance +- All hooks (useState, useCallback, useEffect) must be called at top level +- No conditional hook calls +- Proper dependency arrays for effects + +### Framework Integration +- Use `usePage()` hook for page context (matching CoreDataDictionaryViewer pattern) +- Integrate with `githubService` for repository operations +- Follow PageLayout and component architecture standards +- Proper error handling and loading states + +### FSH File Management +- Use existing `actorDefinitionService` patterns +- Leverage staging ground for uncommitted changes +- Follow FSH syntax and validation rules +- Support FSH templates and snippets + +### WHO Standards Compliance +- Follow WHO SMART Guidelines terminology +- Use official data models from smart-base repository +- Maintain FHIR R4 compatibility +- Support both L2 and L3 representations + +## Implementation Priority + +### Phase 1 (High Priority) +1. Fix React hooks compliance issues in PersonaViewer +2. Add "Create New Persona" functionality +3. Basic FSH editing integration + +### Phase 2 (Medium Priority) +1. Enhanced persona viewer with detailed profiles +2. Requirements modeling integration +3. Validation against smart-base models + +### Phase 3 (Lower Priority) +1. Collaboration features +2. Advanced visualization +3. Export and documentation generation + +## References + +- **WHO SMART Guidelines**: https://www.who.int/teams/digital-health-and-innovation/smart-guidelines +- **smart-base Repository**: https://github.com/WorldHealthOrganization/smart-base +- **GenericPersona Model**: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/fsh/models/GenericPersona.fsh +- **FSH Models**: https://github.com/WorldHealthOrganization/smart-base/tree/main/input/fsh/models +- **FHIR ActorDefinition**: http://hl7.org/fhir/actordefinition.html + +## Related Components + +- **ActorEditor**: Existing component for editing ActorDefinitions +- **actorDefinitionService**: Service layer for actor operations +- **stagingGroundService**: Manages uncommitted changes +- **githubService**: GitHub API integration + +## Notes + +- PersonaViewer and ActorEditor work with the same underlying concept (Generic Personas = ActorDefinition) +- PersonaViewer is for read-only scanning and viewing +- ActorEditor is for creating and editing +- Both should share common utilities and data models +- Terminology: "Persona" (DAK term) = "ActorDefinition" (FHIR resource) + +--- + +**Created**: 2025-01-14 +**Last Updated**: 2025-01-14 +**Status**: Planning Document +**Related Issue**: #996 diff --git a/public/404.html b/public/404.html index dcda7f069..d09394a23 100644 --- a/public/404.html +++ b/public/404.html @@ -68,7 +68,21 @@ return; } - // Optimistic routing: try branch deployment first, fallback to main + // Smart routing: check if first segment is a valid component name + var firstSegment = pathSegments[1]; + + // Load route config to check if it's a valid component + if (typeof window.getSGEXRouteConfig === 'function') { + var routeConfig = window.getSGEXRouteConfig(); + if (routeConfig && routeConfig.isValidComponent && routeConfig.isValidComponent(firstSegment)) { + // Component-first routing: /sgex/{component}/{user}/{repo}/{branch} + var componentRoutePath = pathSegments.slice(1).join('/'); + redirectToSPA('/sgex/', componentRoutePath); + return; + } + } + + // Fallback to optimistic branch routing if not a known component var branch = pathSegments[1]; var component = pathSegments[2]; diff --git a/public/routes-config.json b/public/routes-config.json index e40c0385b..8ed3cda35 100644 --- a/public/routes-config.json +++ b/public/routes-config.json @@ -53,6 +53,10 @@ "faq-demo": { "component": "DAKFAQDemo", "path": "./components/DAKFAQDemo" + }, + "persona-viewer": { + "component": "PersonaViewer", + "path": "./components/PersonaViewer" } }, "standardComponents": { diff --git a/src/components/ActorEditor.js b/src/components/ActorEditor.js index 08d8946e6..a614ba34c 100644 --- a/src/components/ActorEditor.js +++ b/src/components/ActorEditor.js @@ -5,13 +5,13 @@ import { PageLayout, useDAKParams } from './framework'; const ActorEditor = () => { const navigate = useNavigate(); - const { profile, repository, branch } = useDAKParams(); + 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 + // State management - ALL HOOKS MUST BE AT THE TOP const [actorDefinition, setActorDefinition] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -23,39 +23,42 @@ const ActorEditor = () => { const [activeTab, setActiveTab] = useState('basic'); // Initialize component - useEffect(() => { - const initializeEditor = async () => { - setLoading(true); - - try { - if (editActorId) { - // Load existing actor from staging ground - const result = actorDefinitionService.getFromStagingGround(editActorId); - if (result) { - setActorDefinition(result.actorDefinition); - } else { - // Actor not found, create new one - setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); - } + 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 { - // Create new actor setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); } - - // Load list of staged actors - setStagedActors(actorDefinitionService.listStagedActors()); - - } catch (error) { - console.error('Error initializing actor editor:', error); - setErrors({ general: 'Failed to initialize editor' }); + } 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); - }; - - initializeEditor(); + } }, [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 => ({ @@ -80,17 +83,15 @@ const ActorEditor = () => { if (!newDefinition[parentField]) { newDefinition[parentField] = []; } - if (!newDefinition[parentField][index]) { newDefinition[parentField][index] = {}; } - newDefinition[parentField][index][field] = value; return newDefinition; }); }, []); - // Add new item to array fields + // Add new item to array field const addArrayItem = useCallback((field, defaultItem = {}) => { setActorDefinition(prev => ({ ...prev, @@ -98,7 +99,7 @@ const ActorEditor = () => { })); }, []); - // Remove item from array fields + // Remove item from array field const removeArrayItem = useCallback((field, index) => { setActorDefinition(prev => ({ ...prev, @@ -106,62 +107,56 @@ const ActorEditor = () => { })); }, []); - // Validate form + // Validate form data const validateForm = useCallback(() => { - if (!actorDefinition) return false; + const newErrors = {}; - const validation = actorDefinitionService.validateActorDefinition(actorDefinition); + if (!actorDefinition?.id) { + newErrors.id = 'Actor ID is required'; + } - if (!validation.isValid) { - const fieldErrors = {}; - validation.errors.forEach(error => { - if (error.includes('ID')) fieldErrors.id = error; - else if (error.includes('Name')) fieldErrors.name = error; - else if (error.includes('Description')) fieldErrors.description = error; - else if (error.includes('type')) fieldErrors.type = error; - else if (error.includes('role')) fieldErrors.roles = error; - else fieldErrors.general = error; - }); - setErrors(fieldErrors); - return false; + if (!actorDefinition?.name) { + newErrors.name = 'Actor name is required'; } - setErrors({}); - return true; - }, [actorDefinition]); - - // Save actor definition - const handleSave = async () => { - if (!validateForm()) { - return; + if (!actorDefinition?.description) { + newErrors.description = 'Actor description is required'; } - setSaving(true); + if (!actorDefinition?.type) { + newErrors.type = 'Actor type is required'; + } - try { - const result = await actorDefinitionService.saveToStagingGround(actorDefinition); - - if (result.success) { - // Refresh staged actors list - setStagedActors(actorDefinitionService.listStagedActors()); - - // Show success message (could be a toast notification) - alert('Actor definition saved to staging ground successfully!'); - - // Update the URL to reflect we're now editing this actor - if (!editActorId) { - navigate(`/actor-editor/${profile?.login}/${repository?.name}${branch && branch !== 'main' ? `/${branch}` : ''}`); + // Validate roles + if (actorDefinition?.roles) { + actorDefinition.roles.forEach((role, index) => { + if (!role.code) { + newErrors[`roles.${index}.code`] = 'Role code is required'; } - } else { - setErrors({ general: result.error }); - } - } catch (error) { - console.error('Error saving actor definition:', error); - setErrors({ general: 'Failed to save actor definition' }); + 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'; + } + }); } - setSaving(false); - }; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, [actorDefinition]); // Generate FSH preview const generatePreview = useCallback(() => { @@ -170,63 +165,99 @@ const ActorEditor = () => { 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' }); } }, [actorDefinition]); - // Load actor template - const loadTemplate = (template) => { - setActorDefinition({ - ...actorDefinitionService.createEmptyActorDefinition(), - ...template, - metadata: { - ...actorDefinitionService.createEmptyActorDefinition().metadata, - ...template.metadata - } - }); - setErrors({}); - }; - - // Load existing staged actor - const loadStagedActor = (actorId) => { - const result = actorDefinitionService.getFromStagingGround(actorId); - if (result) { - setActorDefinition(result.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); - - // Update URL - navigate(`/actor-editor/${profile?.login}/${repository?.name}${branch && branch !== 'main' ? `/${branch}` : ''}`); } - }; + }, []); // Delete staged actor - const deleteStagedActor = (actorId) => { - if (window.confirm(`Are you sure you want to delete the actor "${actorId}"?`)) { - const success = actorDefinitionService.removeFromStagingGround(actorId); - if (success) { - setStagedActors(actorDefinitionService.listStagedActors()); - - // If we're currently editing this actor, create a new one - if (editActorId === actorId) { - setActorDefinition(actorDefinitionService.createEmptyActorDefinition()); - navigate(`/actor-editor/${profile?.login}/${repository?.name}${branch && branch !== 'main' ? `/${branch}` : ''}`); - } - } + 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); } - }; - - + }, []); - // Redirect if missing required context - use useEffect to avoid render issues - useEffect(() => { - if (!profile || !repository) { - navigate('/'); - } - }, [profile, repository, navigate]); + // 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 ( diff --git a/src/components/DecisionSupportLogicView.js b/src/components/DecisionSupportLogicView.js index db0353218..11cc0fde7 100644 --- a/src/components/DecisionSupportLogicView.js +++ b/src/components/DecisionSupportLogicView.js @@ -31,9 +31,9 @@ const DecisionSupportLogicView = () => { const DecisionSupportLogicViewContent = () => { const navigate = useNavigate(); - const { profile, repository, branch: selectedBranch } = useDAKParams(); + const pageParams = useDAKParams(); - // Component state + // Component state - ALL HOOKS AT THE TOP const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [dakDTCodeSystem, setDakDTCodeSystem] = useState(null); @@ -48,7 +48,10 @@ const DecisionSupportLogicViewContent = () => { const [enhancedFullwidth, setEnhancedFullwidth] = useState(false); const [autoHide, setAutoHide] = useState(false); - // Load DAK decision support data + // Extract profile, repository, branch for use in effects + const { profile, repository, branch: selectedBranch } = pageParams; + + // Load DAK decision support data - MOVED BEFORE EARLY RETURNS useEffect(() => { const loadDecisionSupportData = async () => { if (!repository || !selectedBranch) return; @@ -500,31 +503,6 @@ define "Contraindication Present": }; }; - // Filter and sort variables - useEffect(() => { - if (!dakDTCodeSystem?.concepts) return; - - let filtered = dakDTCodeSystem.concepts.filter(concept => - concept.Code?.toLowerCase().includes(searchTerm.toLowerCase()) || - concept.Display?.toLowerCase().includes(searchTerm.toLowerCase()) || - concept.Definition?.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - // Sort - filtered.sort((a, b) => { - const aVal = a[sortField] || ''; - const bVal = b[sortField] || ''; - - if (sortDirection === 'asc') { - return aVal.localeCompare(bVal); - } else { - return bVal.localeCompare(aVal); - } - }); - - setFilteredVariables(filtered); - }, [dakDTCodeSystem, searchTerm, sortField, sortDirection]); - const handleSort = (field) => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); @@ -627,29 +605,6 @@ define "Contraindication Present": setAutoHide(!autoHide); }; - // Cleanup effect for enhanced fullwidth - useEffect(() => { - return () => { - // Clean up body class on unmount - document.body.classList.remove('enhanced-fullwidth-active'); - }; - }, []); - - // Update body class when enhanced fullwidth changes - useEffect(() => { - if (enhancedFullwidth) { - document.body.classList.add('enhanced-fullwidth-active'); - } else { - document.body.classList.remove('enhanced-fullwidth-active'); - } - - return () => { - document.body.classList.remove('enhanced-fullwidth-active'); - }; - }, [enhancedFullwidth]); - - - if (loading) { return (
diff --git a/src/components/PersonaViewer.css b/src/components/PersonaViewer.css new file mode 100644 index 000000000..0b25254ce --- /dev/null +++ b/src/components/PersonaViewer.css @@ -0,0 +1,288 @@ +.persona-viewer { + background: linear-gradient(135deg, #0078d4 0%, #005a9e 100%); + min-height: 100vh; + color: white; + padding: 0; + margin: 0; +} + +.page-header { + background: rgb(4, 11, 118); + padding: 2rem; + border-bottom: 2px solid rgba(255, 255, 255, 0.1); +} + +.page-header h1 { + margin: 0 0 0.5rem 0; + font-size: 2.5rem; + font-weight: bold; + color: white; +} + +.page-description { + margin: 0.5rem 0 1rem 0; + font-size: 1.2rem; + opacity: 0.9; + color: white; +} + +.repository-info { + background: rgba(255, 255, 255, 0.1); + padding: 1rem; + border-radius: 8px; + margin-top: 1rem; + color: white; +} + +.branch-info { + margin-left: 1rem; + opacity: 0.8; + font-style: italic; +} + +.loading-state, .error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-left: 4px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.scan-status { + font-style: italic; + opacity: 0.8; + margin-top: 0.5rem; + color: white; +} + +.scan-controls { + padding: 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + gap: 1rem; +} + +.rescan-button, .retry-button { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; +} + +.rescan-button:hover, .retry-button:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); +} + +.rescan-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.actors-summary { + padding: 2rem; +} + +.actors-summary h2 { + margin: 0 0 1.5rem 0; + font-size: 2rem; + color: white; +} + +.no-actors { + background: rgba(255, 255, 255, 0.1); + padding: 2rem; + border-radius: 8px; + text-align: center; +} + +.search-info { + margin-top: 1.5rem; + text-align: left; +} + +.search-info h3 { + margin: 0 0 1rem 0; + color: white; +} + +.search-info ul { + list-style: none; + padding: 0; + margin: 0; +} + +.search-info li { + background: rgba(255, 255, 255, 0.05); + margin: 0.5rem 0; + padding: 0.75rem; + border-radius: 4px; + border-left: 3px solid rgba(255, 255, 255, 0.3); +} + +.search-info code { + background: rgba(0, 0, 0, 0.2); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'Courier New', monospace; + color: #ffeb3b; +} + +.actors-list { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); +} + +.actor-card { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 1.5rem; + transition: all 0.3s ease; +} + +.actor-card:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.actor-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.actor-name { + margin: 0; + font-size: 1.3rem; + color: white; + flex: 1; + word-break: break-word; +} + +.actor-type { + background: rgba(255, 255, 255, 0.2); + color: white; + padding: 0.3rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: bold; + margin-left: 1rem; + white-space: nowrap; +} + +.actor-type.fsh { + background: rgba(76, 175, 80, 0.8); +} + +.actor-type.json { + background: rgba(255, 152, 0, 0.8); +} + +.actor-details { + margin-bottom: 1rem; +} + +.actor-id { + margin: 0 0 0.5rem 0; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.9); +} + +.actor-description { + margin: 0; + color: rgba(255, 255, 255, 0.8); + line-height: 1.4; +} + +.actor-source { + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding-top: 1rem; +} + +.source-path { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.9); +} + +.source-link { + color: #81d4fa; + text-decoration: none; + font-family: 'Courier New', monospace; + word-break: break-all; +} + +.source-link:hover { + color: #4fc3f7; + text-decoration: underline; +} + +.resource-type { + margin: 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +/* Responsive design */ +@media (max-width: 768px) { + .actors-list { + grid-template-columns: 1fr; + } + + .page-header { + padding: 1.5rem; + } + + .page-header h1 { + font-size: 2rem; + } + + .actor-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .actor-type { + margin-left: 0; + } + + .scan-controls { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .persona-viewer { + /* Already using dark theme */ + } +} \ No newline at end of file diff --git a/src/components/PersonaViewer.js b/src/components/PersonaViewer.js new file mode 100644 index 000000000..c52327956 --- /dev/null +++ b/src/components/PersonaViewer.js @@ -0,0 +1,393 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import githubService from '../services/githubService'; +import { PageLayout, usePage } from './framework'; +import './PersonaViewer.css'; + +const PersonaViewer = () => { + return ( + + + + ); +}; + +const PersonaViewerContent = () => { + const pageContext = usePage(); + + // All hooks must be called before any early returns + const [actors, setActors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [scanStatus, setScanStatus] = useState(''); + + // Handle case where PageProvider context might be null - AFTER all hooks + if (!pageContext) { + return ( +
+
+

Generic Personas & Actor Definitions

+

+ Initializing page context... +

+
+
+
+

Loading

+

Page framework is initializing. Please wait...

+
+
+
+ ); + } + + const { profile, repository, branch } = pageContext; + + // Get data from page framework + const user = profile?.login; + const repo = repository?.name; + const selectedBranch = branch || repository?.default_branch || 'main'; + + // Helper function to parse FSH file content for actor definitions + const parseFshFileForActors = useCallback((filePath, content) => { + const actors = []; + const lines = content.split('\n'); + + let currentActor = null; + let inActorDefinition = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Look for Profile definitions that could be actors + if (line.startsWith('Profile:') || line.startsWith('Instance:')) { + const id = line.split(':')[1]?.trim(); + if (id) { + // Generous assumption: any instance ID could be an actor + currentActor = { + id, + name: id, + description: '', + type: 'FSH Profile/Instance', + source: { + type: 'fsh', + path: filePath, + lineNumber: i + 1 + } + }; + inActorDefinition = true; + } + } + + // Look for explicit actor-related keywords + if (line.includes('ActorDefinition') || line.includes('Actor') || + line.toLowerCase().includes('persona') || line.toLowerCase().includes('role')) { + if (!currentActor && line.includes(':')) { + const parts = line.split(':'); + const id = parts[parts.length - 1]?.trim(); + if (id) { + currentActor = { + id, + name: id, + description: 'Actor definition found', + type: 'FSH Actor', + source: { + type: 'fsh', + path: filePath, + lineNumber: i + 1 + } + }; + inActorDefinition = true; + } + } + } + + // Extract title and description + if (currentActor && inActorDefinition) { + if (line.startsWith('Title:')) { + currentActor.name = line.split(':')[1]?.trim().replace(/"/g, '') || currentActor.id; + } else if (line.startsWith('Description:')) { + currentActor.description = line.split(':')[1]?.trim().replace(/"/g, '') || ''; + } else if (line.startsWith('Id:')) { + currentActor.id = line.split(':')[1]?.trim() || currentActor.id; + } + + // End of definition (empty line or new definition) + if (line === '' || line.startsWith('Profile:') || line.startsWith('Instance:')) { + if (currentActor.id && i > 0) { + actors.push(currentActor); + currentActor = null; + inActorDefinition = false; + } + } + } + } + + // Add the last actor if we ended the file while in a definition + if (currentActor && currentActor.id) { + actors.push(currentActor); + } + + return actors; + }, []); + + // Helper function to parse JSON file content for actor definitions + const parseJsonFileForActors = useCallback((filePath, content) => { + const actors = []; + + try { + const jsonData = JSON.parse(content); + + // Function to recursively search for actor-like objects + const searchForActors = (obj, path = '') => { + if (typeof obj !== 'object' || obj === null) return; + + if (Array.isArray(obj)) { + obj.forEach((item, index) => searchForActors(item, `${path}[${index}]`)); + return; + } + + // Check if this object looks like an actor definition + const resourceType = obj.resourceType; + const id = obj.id; + + if (resourceType && id) { + // Be generous with actor-like resource types + if (resourceType === 'ActorDefinition' || + resourceType === 'SGActorDefinition' || + resourceType === 'Persona' || + resourceType === 'SGPersona' || + resourceType.toLowerCase().includes('actor') || + resourceType.toLowerCase().includes('persona')) { + + actors.push({ + id: id, + name: obj.name || obj.title || id, + description: obj.description || `${resourceType} resource`, + type: `JSON ${resourceType}`, + source: { + type: 'json', + path: filePath, + resourceType: resourceType, + fullPath: path + } + }); + } + } + + // Recursively search nested objects + Object.keys(obj).forEach(key => { + searchForActors(obj[key], path ? `${path}.${key}` : key); + }); + }; + + searchForActors(jsonData); + + } catch (parseError) { + console.warn(`Failed to parse JSON file ${filePath}:`, parseError); + } + + return actors; + }, []); + + // Scan the repository for actor definitions + const scanForActors = useCallback(async () => { + if (!githubService.isAuth() || !user || !repo) { + setError('GitHub authentication required and repository information needed'); + setLoading(false); + return; + } + + setScanStatus('Starting scan...'); + setActors([]); + + try { + const allActors = []; + + // 1. Scan FSH files under input/fsh/actors + setScanStatus('Scanning FSH files in input/fsh/actors...'); + try { + const actorsDir = await githubService.getDirectoryContents(user, repo, 'input/fsh/actors', selectedBranch); + + for (const file of actorsDir) { + if (file.type === 'file' && file.name.endsWith('.fsh')) { + setScanStatus(`Scanning FSH file: ${file.name}`); + try { + const content = await githubService.getFileContent(user, repo, file.path, selectedBranch); + const fshActors = parseFshFileForActors(file.path, content); + allActors.push(...fshActors); + } catch (fileError) { + console.warn(`Failed to read FSH file ${file.path}:`, fileError); + } + } + } + } catch (dirError) { + console.warn('No input/fsh/actors directory found or access denied:', dirError); + } + + // 2. Scan JSON files under inputs/resources + setScanStatus('Scanning JSON files in inputs/resources...'); + try { + const resourcesDir = await githubService.getDirectoryContents(user, repo, 'inputs/resources', selectedBranch); + + for (const file of resourcesDir) { + if (file.type === 'file' && file.name.endsWith('.json')) { + setScanStatus(`Scanning JSON file: ${file.name}`); + try { + const content = await githubService.getFileContent(user, repo, file.path, selectedBranch); + const jsonActors = parseJsonFileForActors(file.path, content); + allActors.push(...jsonActors); + } catch (fileError) { + console.warn(`Failed to read JSON file ${file.path}:`, fileError); + } + } + } + } catch (dirError) { + console.warn('No inputs/resources directory found or access denied:', dirError); + } + + setScanStatus(`Scan complete. Found ${allActors.length} actors.`); + setActors(allActors); + setError(null); + + } catch (error) { + console.error('Error scanning for actors:', error); + setError(`Failed to scan repository: ${error.message}`); + setScanStatus('Scan failed'); + } finally { + setLoading(false); + } + }, [user, repo, selectedBranch, parseFshFileForActors, parseJsonFileForActors]); + + // Initial scan when component mounts + useEffect(() => { + if (user && repo) { + scanForActors(); + } else { + setLoading(false); + setError('Repository information not available'); + } + }, [user, repo, selectedBranch, scanForActors]); + + // Helper function to generate source file link + const getSourceFileLink = useCallback((actor) => { + if (!user || !repo || !actor.source) return '#'; + + const baseUrl = `https://github.com/${user}/${repo}/blob/${selectedBranch}/${actor.source.path}`; + + if (actor.source.lineNumber) { + return `${baseUrl}#L${actor.source.lineNumber}`; + } + + return baseUrl; + }, [user, repo, selectedBranch]); + + if (loading) { + return ( +
+
+
+

Loading user scenarios and personas...

+ {scanStatus &&

{scanStatus}

} +
+
+ ); + } + + if (error) { + return ( +
+
+

Error

+

{error}

+ {user && repo && ( + + )} +
+
+ ); + } + + return ( +
+
+

Generic Personas & Actor Definitions

+

+ Actor definitions and personas found in this DAK repository for healthcare workflows. +

+ {user && repo && ( +
+ Repository: {user}/{repo} + (branch: {selectedBranch}) +
+ )} +
+ +
+ + {scanStatus &&

{scanStatus}

} +
+ +
+

Found Personas & Actors ({actors.length})

+ {actors.length === 0 ? ( +
+

No actor definitions or personas found in this repository.

+
+

Searched in:

+
    +
  • input/fsh/actors/*.fsh - FSH actor definitions (generous matching)
  • +
  • inputs/resources/*.json - JSON ActorDefinition resources (strict matching)
  • +
+
+
+ ) : ( +
+ {actors.map((actor, index) => ( +
+
+

{actor.name}

+ + {actor.type} + +
+ +
+

ID: {actor.id}

+ {actor.description && ( +

{actor.description}

+ )} +
+ +
+

+ Source: + + {actor.source.path} + {actor.source.lineNumber && ` (line ${actor.source.lineNumber})`} + +

+ {actor.source.resourceType && ( +

+ Resource Type: {actor.source.resourceType} +

+ )} +
+
+ ))} +
+ )} +
+
+ ); +}; + +export default PersonaViewer; \ No newline at end of file diff --git a/src/components/QuestionnaireEditor.js b/src/components/QuestionnaireEditor.js index 75dcf6d69..a6659b091 100644 --- a/src/components/QuestionnaireEditor.js +++ b/src/components/QuestionnaireEditor.js @@ -287,21 +287,50 @@ const LFormsVisualEditor = ({ questionnaire, onChange }) => { }; const QuestionnaireEditorContent = () => { - const { repository, branch, isLoading: pageLoading } = useDAKParams(); + const pageParams = useDAKParams(); - // Component state + // Component state - ALL HOOKS MUST BE AT THE TOP const [questionnaires, setQuestionnaires] = useState([]); const [selectedQuestionnaire, setSelectedQuestionnaire] = useState(null); const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - const [editing, setEditing] = useState(false); - const [questionnaireContent, setQuestionnaireContent] = useState(null); - const [originalContent, setOriginalContent] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [showPreview, setShowPreview] = useState(false); + + // 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.

+
+
+ ); + } - // LForms integration state + if (pageParams.loading) { + return ( +
+
+

Loading...

+

Initializing page context...

+
+
+ ); + } + + const { repository, branch, isLoading: pageLoading } = pageParams; + + // LForms integration state (additional state) const [lformsLoaded, setLformsLoaded] = useState(false); const [editMode, setEditMode] = useState('visual'); // 'visual' or 'json' const [lformsError, setLformsError] = useState(null); + const [editing, setEditing] = useState(false); + const [questionnaireContent, setQuestionnaireContent] = useState(null); + const [originalContent, setOriginalContent] = useState(null); // Check if we have the necessary context data const hasRequiredData = repository && branch && !pageLoading; diff --git a/src/components/framework/usePageParams.js b/src/components/framework/usePageParams.js index 1e0d8f341..0c88706fc 100644 --- a/src/components/framework/usePageParams.js +++ b/src/components/framework/usePageParams.js @@ -60,12 +60,41 @@ export const useDAKParams = () => { try { const pageParams = usePageParams(); + // Handle case where PageProvider context is null or not properly initialized + if (!pageParams || !pageParams.type) { + console.warn('useDAKParams: PageProvider context not available, returning empty data'); + return { + user: null, + profile: null, + repository: null, + branch: null, + asset: null, + updateBranch: () => {}, + navigate: () => {}, + loading: true, + error: null + }; + } + // Only throw error if page is fully loaded and type is not DAK/ASSET - // This prevents errors during initial loading or page type determination + // But allow loading state to pass through if (!pageParams.loading && pageParams.type !== PAGE_TYPES.DAK && pageParams.type !== PAGE_TYPES.ASSET) { - throw new Error(`useDAKParams can only be used on DAK or Asset pages. Current page type: ${pageParams.type}`); + + // Instead of throwing, return null data with error flag for graceful degradation + console.warn(`useDAKParams: Component loaded on ${pageParams.type} page instead of DAK/Asset page. Returning empty data for graceful degradation.`); + return { + user: null, + profile: null, + repository: null, + branch: null, + asset: null, + updateBranch: () => {}, + navigate: pageParams.navigate || (() => {}), + loading: false, + error: `This component requires a DAK or Asset page context but was loaded on a ${pageParams.type} page.` + }; } return { @@ -75,23 +104,24 @@ export const useDAKParams = () => { branch: pageParams.branch, asset: pageParams.asset, updateBranch: pageParams.updateBranch, - navigate: pageParams.navigate + navigate: pageParams.navigate, + loading: pageParams.loading || false, + error: null }; } catch (error) { - // If PageProvider is not ready yet, return empty object - if (error.message.includes('usePage must be used within a PageProvider')) { - console.log('useDAKParams: PageProvider not ready yet, returning empty data'); - return { - user: null, - profile: null, - repository: null, - branch: null, - asset: null, - updateBranch: () => {}, - navigate: () => {} - }; - } - throw error; + // If PageProvider is not ready yet, return empty object with loading state + console.warn('useDAKParams: PageProvider error, returning empty data:', error.message); + return { + user: null, + profile: null, + repository: null, + branch: null, + asset: null, + updateBranch: () => {}, + navigate: () => {}, + loading: true, + error: null + }; } }; diff --git a/src/services/componentRouteService.js b/src/services/componentRouteService.js index 0877c44e6..622d3b646 100644 --- a/src/services/componentRouteService.js +++ b/src/services/componentRouteService.js @@ -119,6 +119,9 @@ function createLazyComponent(componentName) { case 'QuestionnaireEditor': LazyComponent = React.lazy(() => import('../components/QuestionnaireEditor')); break; + case 'PersonaViewer': + LazyComponent = React.lazy(() => import('../components/PersonaViewer')); + break; default: console.warn(`Unknown component ${componentName}, using fallback`);