diff --git a/PROGRAM_INDICATORS_MIGRATION_PLAN.md b/PROGRAM_INDICATORS_MIGRATION_PLAN.md new file mode 100644 index 0000000000..c841d0aac7 --- /dev/null +++ b/PROGRAM_INDICATORS_MIGRATION_PLAN.md @@ -0,0 +1,273 @@ +# Program Indicators Component Migration Plan + +## Overview + +This document outlines the migration plan for updating the Program Indicators components to use the new DAK Component Object architecture introduced in PR #1111. + +## Current Implementation + +The current implementation (commits `caae430` and `9a553e1`) includes: + +1. **ProgramIndicatorsViewer.js**: Lists measures from `input/fsh/measures/` directory + - Directly uses `githubService.getContents()` and `githubService.getFileContent()` + - Manually parses FSH files to extract Title + - Custom FSH parsing logic + +2. **ProgramIndicatorEditor.js**: Edits program indicators with FSH preview + - Directly uses `githubService.getFileContent()` for loading + - Uses `stagingGroundService.updateFile()` for saving + - Manual FSH parsing with `parseFSHToIndicatorData()` + - Manual FSH generation with `generateFSHFromIndicatorData()` + +## Target Architecture (from PR #1111) + +According to PR #1111, the new architecture includes: + +### ProgramIndicatorComponent Class +- Location: `packages/dak-core/src/components/ProgramIndicatorComponent.ts` +- Handles JSON indicators with numerator/denominator +- Directory: `input/indicators/` +- Methods: + - `getSources()` - Get all indicator sources + - `addSource(source)` - Add new indicator source + - `retrieveAll()` - Retrieve all indicators + - `retrieveById(id)` - Get specific indicator + - `save(indicator)` - Save indicator + - `validate(indicator)` - Validate indicator data + +### React Integration +- **ComponentObjectProvider** - React context for DAK object access +- **useDakComponent('indicators')** - Hook to access ProgramIndicatorComponent +- **editorIntegrationService** - Bridge between React and TypeScript + +### DAK Object Integration +- DAKObject manages all 9 component types +- Automatic dak.json updates through component callbacks +- Source types: canonical URL, relative URL, inline instance data + +## Migration Tasks + +### Phase 1: Update ProgramIndicatorsViewer.js + +**Current approach:** +```javascript +// Direct GitHub API usage +const files = await githubService.getContents(owner, repoName, 'input/fsh/measures', ref); +const content = await githubService.getFileContent(owner, repoName, file.path, ref); +const title = extractTitleFromFSH(content); +``` + +**New approach:** +```javascript +// Use ProgramIndicatorComponent +import { useDakComponent } from '../services/editorIntegrationService'; + +const component = useDakComponent('indicators'); +const indicators = await component.retrieveAll(); +// Indicators are already parsed with all metadata +``` + +**Changes needed:** +1. Import `useDakComponent` hook +2. Replace `githubService` calls with `component.retrieveAll()` +3. Remove manual FSH parsing logic (`extractTitleFromFSH`) +4. Update data structure to work with ProgramIndicator objects +5. Update navigation to pass indicator ID instead of file path + +### Phase 2: Update ProgramIndicatorEditor.js + +**Current approach:** +```javascript +// Manual loading and parsing +const content = await githubService.getFileContent(owner, repoName, decodedPath, ref); +const parsed = parseFSHToIndicatorData(content); + +// Manual saving +stagingGroundService.updateFile(decodedPath, content, {...}); +``` + +**New approach:** +```javascript +// Use ProgramIndicatorComponent +const component = useDakComponent('indicators'); +const indicator = await component.retrieveById(indicatorId); + +// Edit indicator object directly +indicator.name = newName; +indicator.numerator = newNumerator; + +// Save with automatic dak.json update +await component.save(indicator); +``` + +**Changes needed:** +1. Import `useDakComponent` hook +2. Change from file path to indicator ID in URL params +3. Replace `githubService.getFileContent()` with `component.retrieveById()` +4. Work with ProgramIndicator object structure instead of FSH content +5. Replace manual FSH parsing/generation +6. Replace `stagingGroundService.updateFile()` with `component.save()` +7. Remove FSH preview panel (or keep as read-only generated FSH) +8. Update form fields to match ProgramIndicator schema + +### Phase 3: Update Data Model + +**Current ProgramIndicator fields (our interpretation):** +```javascript +{ + id: string, + name: string, + definition: string, + numerator: string, + denominator: string, + disaggregation: string, + descriptionString: string, + descriptionUri: string, + references: string[] +} +``` + +**Actual ProgramIndicator schema (from smart-base):** +Need to verify the exact schema in `packages/dak-core/src/schemas/` or the Component class. + +According to PR #1111, ProgramIndicatorComponent handles "JSON indicators with numerator/denominator from `input/indicators/`". + +This means indicators are stored as JSON files, not FSH files! + +**Critical Discovery:** The current implementation is based on FHIR Measure FSH files in `input/fsh/measures/`, but the new Component Object architecture uses JSON files in `input/indicators/`. This is a significant change. + +### Phase 4: Reconcile FSH vs JSON Format + +**Issue:** Original requirement mentions "Measure instance fsh" but PR #1111 indicates JSON format. + +**Options:** +1. **Option A:** Update to use JSON format exclusively + - Store indicators as JSON in `input/indicators/` + - Remove FSH parsing/generation + - Simpler data model + +2. **Option B:** Support both formats + - JSON for LM data (`input/indicators/`) + - FSH for FHIR Measure resources (`input/fsh/measures/`) + - Maintain mapping between formats + +3. **Option C:** Generate FSH from JSON + - Edit JSON indicators + - Auto-generate FSH Measure files + - Requires FSH generation logic + +**Recommendation:** Clarify with stakeholders which format(s) should be supported. + +### Phase 5: Update Routing + +**Current routes:** +- Viewer: `/program-indicators/:user/:repo/:branch` +- Editor: `/program-indicator-editor/:user/:repo/:branch/*` (file path) + +**New routes:** +- Viewer: `/program-indicators/:user/:repo/:branch` +- Editor: `/program-indicator-editor/:user/:repo/:branch/:indicatorId` + +Change from file path to indicator ID. + +### Phase 6: Update Field Mapping + +**Current mapping (FSH-based):** +- `id` → `Instance: {id}` +- `name` → `Title: "{name}"` +- `definition` → `* description = "{definition}"` +- etc. + +**New mapping (JSON-based):** +Need to understand the actual JSON schema for ProgramIndicator from the Component class. + +### Phase 7: Testing Updates + +Update tests to use Component Object mocks instead of githubService mocks. + +## Questions for Stakeholders + +1. **Format Clarification:** Should indicators be stored as JSON (per Component Object architecture) or FSH (per original requirement)? Or both? + +2. **Directory Location:** Should we use `input/indicators/` (Component Object default) or `input/fsh/measures/` (original requirement)? + +3. **FSH Generation:** If using JSON format, should we auto-generate FSH Measure files for FHIR IG compilation? + +4. **Migration Path:** Should we: + - Completely replace current implementation? + - Support both old and new formats during transition? + - Provide migration tool for existing FSH files? + +5. **dak.json Updates:** The Component Object architecture automatically updates dak.json. Should we keep or remove the manual dak.json update logic in our current implementation? + +## Implementation Steps (After Main Merge) + +1. **Merge main branch** into `copilot/add-dak-component-indicators` +2. **Verify ProgramIndicatorComponent** exists and review its API +3. **Decide on format** (JSON vs FSH) based on stakeholder input +4. **Update ProgramIndicatorsViewer** to use `useDakComponent('indicators')` +5. **Update ProgramIndicatorEditor** to use Component Object API +6. **Update tests** to work with new architecture +7. **Remove deprecated code** (manual FSH parsing, direct githubService calls) +8. **Test end-to-end** workflow +9. **Update documentation** with new approach + +## Files to Modify + +1. `src/components/ProgramIndicatorsViewer.js` - Use Component Object +2. `src/components/ProgramIndicatorEditor.js` - Use Component Object +3. `src/components/ProgramIndicatorsViewer.css` - May need updates for JSON data +4. `src/components/ProgramIndicatorEditor.css` - May need updates if UI changes +5. Tests (if any) - Update to use Component Object mocks + +## Files to Potentially Remove + +1. Manual FSH parsing functions in `ProgramIndicatorEditor.js` +2. Manual FSH generation functions in `ProgramIndicatorEditor.js` +3. Direct `githubService` usage for indicator files +4. Direct `stagingGroundService` usage (replaced by Component Object) + +## Expected Benefits + +1. **Automatic dak.json updates** - No manual sync needed +2. **Consistent data model** - All indicators use same structure +3. **Better validation** - Component Object provides built-in validation +4. **Easier testing** - Mock Component Object instead of multiple services +5. **Code reduction** - Remove 200-300 lines of manual FSH parsing/generation +6. **Type safety** - TypeScript types for indicator data + +## Risks and Mitigation + +**Risk 1: Breaking changes in data format** +- Mitigation: Provide migration script for existing data + +**Risk 2: Loss of FSH editing capability** +- Mitigation: Keep FSH preview, or provide FSH export feature + +**Risk 3: Different directory structure** +- Mitigation: Document new structure, provide migration guide + +**Risk 4: User workflow disruption** +- Mitigation: Maintain similar UI/UX, clear documentation + +## Timeline Estimate + +- Phase 1-2: 4-6 hours (Update both components) +- Phase 3-4: 2-3 hours (Data model reconciliation) +- Phase 5-6: 2-3 hours (Routing and mapping updates) +- Phase 7: 2-3 hours (Testing) +- Total: 10-15 hours of development + +## Next Steps + +1. **Immediate:** Merge main branch to access new Component Object architecture +2. **Clarify:** Get stakeholder input on JSON vs FSH format question +3. **Plan:** Create detailed implementation tickets +4. **Execute:** Implement changes following this plan +5. **Review:** Test and validate all changes +6. **Document:** Update user documentation with new approach + +--- + +**Status:** Waiting for main branch merge and stakeholder clarification on format questions. +**Last Updated:** 2025-10-16 diff --git a/public/routes-config.json b/public/routes-config.json index c196e8e844..78331a03da 100644 --- a/public/routes-config.json +++ b/public/routes-config.json @@ -58,6 +58,14 @@ "persona-viewer": { "component": "PersonaViewer", "path": "./components/PersonaViewer" + }, + "program-indicators": { + "component": "ProgramIndicatorsViewer", + "path": "./components/ProgramIndicatorsViewer" + }, + "program-indicator-editor": { + "component": "ProgramIndicatorEditor", + "path": "./components/ProgramIndicatorEditor" } }, "standardComponents": { diff --git a/src/components/DAKDashboard.js b/src/components/DAKDashboard.js index 7d1f44f6c6..389c189aa7 100644 --- a/src/components/DAKDashboard.js +++ b/src/components/DAKDashboard.js @@ -506,6 +506,18 @@ const DAKDashboardContent = () => { return; } + // For program-indicators, navigate to indicators viewer + if (component.id === 'program-indicators') { + const owner = repository.owner?.login || repository.full_name.split('/')[0]; + const repoName = repository.name; + const path = selectedBranch + ? `/program-indicators/${owner}/${repoName}/${selectedBranch}` + : `/program-indicators/${owner}/${repoName}`; + + handleNavigationClick(event, path, navigate, navigationState); + return; + } + // For other components, check permissions before proceeding if (!hasWriteAccess) { // If command-click, still show permission dialog instead of opening new tab diff --git a/src/components/ProgramIndicatorEditor.css b/src/components/ProgramIndicatorEditor.css new file mode 100644 index 0000000000..1ea45a375e --- /dev/null +++ b/src/components/ProgramIndicatorEditor.css @@ -0,0 +1,276 @@ +/* Program Indicator Editor Styles */ + +.program-indicator-editor { + padding: 1rem; + max-width: 1800px; + margin: 0 auto; +} + +.editor-header { + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.editor-header h2 { + margin: 0 0 0.5rem 0; + color: #333; +} + +.editor-header p { + margin: 0; + color: #666; + font-size: 0.95rem; +} + +.editor-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + min-height: 600px; +} + +.editor-panel { + background: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 1.5rem; + overflow-y: auto; + max-height: calc(100vh - 250px); +} + +.editor-panel h3 { + margin: 0 0 1rem 0; + color: #0078d4; + font-size: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #0078d4; +} + +/* LM Fields Panel */ +.lm-fields-panel { + background: #f8f9fa; +} + +.field-mapping-note { + background: #e7f3ff; + border-left: 4px solid #0078d4; + padding: 0.75rem; + margin-bottom: 1.5rem; + border-radius: 4px; +} + +.field-mapping-note small { + color: #555; + font-size: 0.875rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #333; + font-size: 0.95rem; +} + +.form-group label strong { + color: #0078d4; +} + +.form-group label .required { + color: #d32f2f; + margin-left: 0.25rem; +} + +.form-group label small { + display: block; + color: #666; + font-weight: normal; + font-size: 0.85rem; + margin-top: 0.25rem; +} + +.form-control { + width: 100%; + padding: 0.75rem; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 0.95rem; + font-family: inherit; + transition: border-color 0.2s; +} + +.form-control:focus { + outline: none; + border-color: #0078d4; +} + +.form-control:invalid { + border-color: #d32f2f; +} + +textarea.form-control { + font-family: 'Courier New', monospace; + resize: vertical; +} + +/* Field Mapping Help Table */ +.field-mapping-help { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid #ddd; +} + +.field-mapping-help h4 { + margin: 0 0 1rem 0; + color: #333; + font-size: 1rem; +} + +.mapping-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.mapping-table th, +.mapping-table td { + padding: 0.5rem; + text-align: left; + border: 1px solid #ddd; +} + +.mapping-table th { + background: #0078d4; + color: white; + font-weight: 600; +} + +.mapping-table td { + background: white; +} + +.mapping-table code { + background: #f5f5f5; + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.85rem; + color: #d32f2f; +} + +/* FSH Preview Panel */ +.fsh-preview-panel { + background: #1e1e1e; + color: #d4d4d4; +} + +.fsh-preview-panel h3 { + color: #4ec9b0; + border-bottom-color: #4ec9b0; +} + +.fsh-preview { + font-family: 'Courier New', Consolas, monospace; + font-size: 0.9rem; + line-height: 1.5; + overflow-x: auto; +} + +.fsh-preview pre { + margin: 0; + padding: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +.fsh-preview code { + color: #d4d4d4; +} + +/* Loading and Error States */ +.editor-loading, +.editor-error { + padding: 3rem; + text-align: center; +} + +.editor-error { + background: #fff3cd; + border: 2px solid #ffc107; + border-radius: 8px; + color: #856404; +} + +.editor-error h3 { + margin: 0 0 1rem 0; + color: #856404; +} + +.editor-error button { + margin-top: 1rem; + padding: 0.6rem 1.5rem; + background: #0078d4; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; +} + +.editor-error button:hover { + background: #005a9e; +} + +.loading-spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #0078d4; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .editor-layout { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .editor-panel { + max-height: 500px; + } + + .fsh-preview-panel { + order: -1; /* Show FSH preview first on mobile */ + } +} + +@media (max-width: 768px) { + .program-indicator-editor { + padding: 0.5rem; + } + + .editor-panel { + padding: 1rem; + } + + .mapping-table { + font-size: 0.75rem; + } + + .mapping-table th, + .mapping-table td { + padding: 0.4rem; + } +} diff --git a/src/components/ProgramIndicatorEditor.js b/src/components/ProgramIndicatorEditor.js new file mode 100644 index 0000000000..b102a6be3f --- /dev/null +++ b/src/components/ProgramIndicatorEditor.js @@ -0,0 +1,460 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { AssetEditorLayout, usePage } from './framework'; +import githubService from '../services/githubService'; +import stagingGroundService from '../services/stagingGroundService'; +import './ProgramIndicatorEditor.css'; + +const ProgramIndicatorEditor = () => { + const navigate = useNavigate(); + const { repository, branch } = usePage(); + const { '*': assetPath } = useParams(); // Get the file path from URL + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [originalContent, setOriginalContent] = useState(''); + const [fshContent, setFshContent] = useState(''); + + // ProgramIndicator LM fields + const [indicatorData, setIndicatorData] = useState({ + id: '', + name: '', + definition: '', + numerator: '', + denominator: '', + disaggregation: '', + descriptionString: '', + descriptionUri: '', + references: [] + }); + + // Load measure file + useEffect(() => { + const loadMeasureFile = async () => { + if (!repository || !assetPath) { + navigate('/'); + return; + } + + try { + setLoading(true); + setError(null); + + const owner = repository.owner?.login || repository.full_name.split('/')[0]; + const repoName = repository.name; + const ref = branch || 'main'; + + // Decode the asset path + const decodedPath = decodeURIComponent(assetPath); + + console.log('Loading measure file:', { owner, repoName, path: decodedPath, ref }); + + // Get file content + const content = await githubService.getFileContent(owner, repoName, decodedPath, ref); + + setOriginalContent(content); + setFshContent(content); + + // Parse FSH to extract ProgramIndicator LM data + const parsed = parseFSHToIndicatorData(content); + setIndicatorData(parsed); + + setLoading(false); + } catch (err) { + console.error('Error loading measure file:', err); + setError(err.message); + setLoading(false); + } + }; + + loadMeasureFile(); + }, [repository, branch, assetPath, navigate]); + + // Parse FSH content to extract ProgramIndicator LM fields + const parseFSHToIndicatorData = (fshContent) => { + const data = { + id: '', + name: '', + definition: '', + numerator: '', + denominator: '', + disaggregation: '', + descriptionString: '', + descriptionUri: '', + references: [] + }; + + if (!fshContent) return data; + + // Extract Instance name (ID) + const instanceMatch = fshContent.match(/^Instance:\s+(\S+)/m); + if (instanceMatch) { + data.id = instanceMatch[1]; + } + + // Extract Title (maps to name) + const titleMatch = fshContent.match(/^Title:\s*"(.+)"$/m); + if (titleMatch) { + data.name = titleMatch[1]; + } + + // Extract description + const descMatch = fshContent.match(/^\*\s*description\s*=\s*"(.+)"$/m); + if (descMatch) { + data.definition = descMatch[1]; + } + + // Extract numerator description + const numeratorMatch = fshContent.match(/population\[numerator\][\s\S]*?description\s*=\s*"(.+?)"/); + if (numeratorMatch) { + data.numerator = numeratorMatch[1]; + } + + // Extract denominator description + const denominatorMatch = fshContent.match(/population\[denominator\][\s\S]*?description\s*=\s*"(.+?)"/); + if (denominatorMatch) { + data.denominator = denominatorMatch[1]; + } + + // Look for comments that might contain disaggregation or references + const commentMatches = fshContent.matchAll(/^\/\/\s*(.+)$/gm); + for (const match of commentMatches) { + const comment = match[1].trim(); + if (comment.toLowerCase().startsWith('disaggregation:')) { + data.disaggregation = comment.substring('disaggregation:'.length).trim(); + } else if (comment.toLowerCase().startsWith('references:')) { + const refs = comment.substring('references:'.length).trim(); + data.references = refs.split(',').map(r => r.trim()).filter(r => r); + } + } + + return data; + }; + + // Generate FSH content from ProgramIndicator LM data + const generateFSHFromIndicatorData = (data) => { + // Keep the original FSH structure but update specific fields + let newFsh = fshContent; + + // Update Instance name + if (data.id && data.id !== indicatorData.id) { + newFsh = newFsh.replace(/^Instance:\s+\S+/m, `Instance: ${data.id}`); + } + + // Update Title + if (data.name) { + newFsh = newFsh.replace(/^Title:\s*".+"$/m, `Title: "${data.name}"`); + newFsh = newFsh.replace(/^\*\s*title\s*=\s*".+"$/m, `* title = "${data.name}"`); + newFsh = newFsh.replace(/^\*\s*name\s*=\s*".+"$/m, `* name = "${data.id}"`); + } + + // Update description (definition) + if (data.definition) { + newFsh = newFsh.replace(/^\*\s*description\s*=\s*".+"$/m, `* description = "${data.definition}"`); + } + + // Update numerator description + if (data.numerator) { + newFsh = newFsh.replace( + /(population\[numerator\][\s\S]*?description\s*=\s*")(.+?)(")/, + `$1${data.numerator}$3` + ); + } + + // Update denominator description + if (data.denominator) { + newFsh = newFsh.replace( + /(population\[denominator\][\s\S]*?description\s*=\s*")(.+?)(")/, + `$1${data.denominator}$3` + ); + } + + // Add comments for disaggregation and references if they don't exist + if (data.disaggregation || data.references.length > 0) { + const lines = newFsh.split('\n'); + const titleLineIndex = lines.findIndex(line => line.startsWith('Title:')); + + if (titleLineIndex >= 0) { + const commentsToAdd = []; + if (data.disaggregation) { + commentsToAdd.push(`// Disaggregation: ${data.disaggregation}`); + } + if (data.references.length > 0) { + commentsToAdd.push(`// References: ${data.references.join(', ')}`); + } + + // Insert comments after Title line + lines.splice(titleLineIndex + 1, 0, ...commentsToAdd); + newFsh = lines.join('\n'); + } + } + + return newFsh; + }; + + // Handle field changes + const handleFieldChange = (field, value) => { + const newData = { ...indicatorData, [field]: value }; + setIndicatorData(newData); + + // Update FSH content + const newFsh = generateFSHFromIndicatorData(newData); + setFshContent(newFsh); + }; + + // Handle save + const handleSave = async (content, saveType) => { + try { + if (saveType === 'local') { + // Save to staging ground + const decodedPath = decodeURIComponent(assetPath); + stagingGroundService.updateFile(decodedPath, content, { + source: 'program-indicator-editor', + timestamp: Date.now() + }); + + // TODO: Update dak.json indicators array + console.log('Saved to staging ground:', decodedPath); + return { result: 'success' }; + } else { + // GitHub save would go here, but we're only saving to staging ground per requirements + console.log('GitHub save not yet implemented'); + return { result: 'error', message: 'GitHub save not implemented' }; + } + } catch (error) { + console.error('Save error:', error); + return { result: 'error', message: error.message }; + } + }; + + const hasChanges = fshContent !== originalContent; + + if (loading) { + return ( + +
+
+

Loading measure...

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

Error Loading Measure

+

{error}

+ +
+
+ ); + } + + const fileName = assetPath ? decodeURIComponent(assetPath).split('/').pop() : 'measure.fsh'; + + return ( + +
+
+

Program Indicator Editor

+

Edit ProgramIndicator Logical Model fields mapped to FHIR Measure FSH

+
+ +
+ {/* Left side: Editable LM fields */} +
+

ProgramIndicator LM Fields

+
+ WHO SMART Base ProgramIndicator Logical Model fields mapped to FHIR Measure FSH +
+ +
+ + handleFieldChange('id', e.target.value)} + pattern="[A-Za-z0-9\-\.]{1,64}" + className="form-control" + /> +
+ +
+ + handleFieldChange('name', e.target.value)} + className="form-control" + /> +
+ +
+ +