From 6a5140eb2538db35f01fe3294062bfda5ffd250b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 23:14:23 +0000 Subject: [PATCH 01/16] Initial plan From 2566d92f73185ea1e2699b566a2cee4d29f95617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 23:26:36 +0000 Subject: [PATCH 02/16] Add RequirementsEditor component with FSH file management Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com> --- public/docs/dak-components.md | 7 +- src/components/RequirementsEditor.css | 363 ++++++++++++++++++++++ src/components/RequirementsEditor.js | 421 ++++++++++++++++++++++++++ src/services/componentRouteService.js | 3 + src/services/helpContentService.js | 147 +++++++++ 5 files changed, 940 insertions(+), 1 deletion(-) create mode 100644 src/components/RequirementsEditor.css create mode 100644 src/components/RequirementsEditor.js diff --git a/public/docs/dak-components.md b/public/docs/dak-components.md index b8dbb26d30..3d3bc01c62 100644 --- a/public/docs/dak-components.md +++ b/public/docs/dak-components.md @@ -90,7 +90,12 @@ Digital Adaptation Kits (DAKs) are structured packages of clinical logic and imp - **L2 Representation**: Requirements specifications at https://worldhealthorganization.github.io/smart-base/StructureDefinition-FunctionalRequirement.html and https://worldhealthorganization.github.io/smart-base/StructureDefinition-NonFunctionalRequirement.html - **L3 Representation**: FHIR ImplementationGuide conformance rules - **Purpose**: Document system capabilities, performance requirements, and technical constraints -- **Editor**: Requirements editor with structured templates +- **Editor**: Requirements editor with FSH file management for creating, editing, and deleting functional and non-functional requirements +- **File Location**: `input/fsh/requirements/*.fsh` +- **Models**: Based on WHO smart-base logical models + - **FunctionalRequirement**: Defines system capabilities with id, activity, actor, capability ("I want"), benefit ("so that"), and classification + - **NonFunctionalRequirement**: Defines system qualities with id, requirement description, category, and classification +- **Extraction Tool**: WHO smart-base provides `req_extractor.py` for processing requirements from Excel sheets ### 9. Test Scenarios diff --git a/src/components/RequirementsEditor.css b/src/components/RequirementsEditor.css new file mode 100644 index 0000000000..6d56d87183 --- /dev/null +++ b/src/components/RequirementsEditor.css @@ -0,0 +1,363 @@ +/* Requirements Editor Styles */ +.requirements-editor { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.requirements-editor-loading { + text-align: center; + padding: 4rem 2rem; + color: white; +} + +.requirements-editor-loading .loading-spinner { + border: 4px solid rgba(255, 255, 255, 0.3); + border-top: 4px solid white; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-message { + background: #ffebee; + border: 1px solid #e57373; + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; + color: #c62828; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.error-icon { + font-size: 1.5rem; +} + +/* Requirements Layout */ +.requirements-layout { + display: grid; + grid-template-columns: 300px 1fr; + gap: 1rem; + height: calc(100vh - 200px); + min-height: 600px; +} + +/* Sidebar */ +.requirements-sidebar { + background: white; + border-radius: 0.75rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.sidebar-header h3 { + margin: 0 0 1rem 0; + color: #333; + font-size: 1.2rem; +} + +.create-buttons { + display: flex; + gap: 0.5rem; + flex-direction: column; +} + +.btn-create-functional, +.btn-create-nonfunctional { + background: #0078d4; + color: white; + border: none; + padding: 0.5rem 0.75rem; + border-radius: 0.4rem; + font-weight: 500; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s ease; +} + +.btn-create-nonfunctional { + background: #107c10; +} + +.btn-create-functional:hover { + background: #106ebe; +} + +.btn-create-nonfunctional:hover { + background: #0d5c0d; +} + +.requirements-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.no-requirements { + padding: 2rem 1rem; + text-align: center; + color: #666; +} + +.no-requirements p { + margin: 0.5rem 0; + font-size: 0.9rem; + line-height: 1.5; +} + +.requirement-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + margin-bottom: 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + background: #f8f9fa; +} + +.requirement-item:hover { + background: #e9ecef; +} + +.requirement-item.selected { + background: #0078d4; + color: white; +} + +.requirement-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.requirement-name { + font-size: 0.9rem; + font-weight: 500; + word-break: break-word; +} + +/* Editor Panel */ +.requirements-editor-panel { + background: white; + border-radius: 0.75rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.requirements-welcome { + padding: 3rem; + color: #333; +} + +.requirements-welcome h2 { + margin: 0 0 1rem 0; + color: rgb(4, 11, 118); +} + +.requirements-welcome p { + margin-bottom: 2rem; + line-height: 1.6; +} + +.requirements-info { + background: #f8f9fa; + padding: 2rem; + border-radius: 0.5rem; +} + +.requirements-info h3 { + margin: 0 0 1rem 0; + color: rgb(4, 11, 118); +} + +.requirements-info ul { + margin: 1rem 0; + padding-left: 1.5rem; +} + +.requirements-info li { + margin: 0.5rem 0; + line-height: 1.6; +} + +.requirements-info code { + background: #e9ecef; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + font-family: 'Courier New', monospace; +} + +/* Requirement Editor */ +.requirement-editor { + display: flex; + flex-direction: column; + height: 100%; +} + +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.editor-header h3 { + margin: 0; + color: #333; + font-size: 1.2rem; +} + +.editor-actions { + display: flex; + gap: 0.5rem; +} + +.btn-save, +.btn-cancel, +.btn-delete { + border: none; + padding: 0.5rem 1rem; + border-radius: 0.4rem; + font-weight: 500; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.btn-save { + background: #107c10; + color: white; +} + +.btn-save:hover { + background: #0d5c0d; +} + +.btn-cancel { + background: #e0e0e0; + color: #333; +} + +.btn-cancel:hover { + background: #d0d0d0; +} + +.btn-delete { + background: #d32f2f; + color: white; +} + +.btn-delete:hover { + background: #b71c1c; +} + +.editor-content { + flex: 1; + padding: 1rem; + overflow: auto; +} + +.fsh-editor { + width: 100%; + height: 100%; + min-height: 400px; + padding: 1rem; + border: 1px solid #e0e0e0; + border-radius: 0.5rem; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + line-height: 1.6; + resize: vertical; +} + +.fsh-editor:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); +} + +.editor-help { + padding: 1.5rem; + border-top: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.editor-help h4 { + margin: 0 0 0.75rem 0; + color: rgb(4, 11, 118); + font-size: 1rem; +} + +.editor-help p { + margin: 0.5rem 0; + line-height: 1.6; +} + +.editor-help ul { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.editor-help li { + margin: 0.25rem 0; + line-height: 1.6; +} + +.editor-help code { + background: #e9ecef; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + font-family: 'Courier New', monospace; + color: #d32f2f; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .requirements-layout { + grid-template-columns: 1fr; + height: auto; + } + + .requirements-sidebar { + max-height: 300px; + } +} + +@media (max-width: 768px) { + .requirements-welcome { + padding: 2rem 1rem; + } + + .editor-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .editor-actions { + width: 100%; + justify-content: flex-end; + } +} diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js new file mode 100644 index 0000000000..04f4e073cf --- /dev/null +++ b/src/components/RequirementsEditor.js @@ -0,0 +1,421 @@ +import React, { useState, useEffect } from 'react'; +import { PageLayout, AssetEditorLayout, useDAKParams } from './framework'; +import ContextualHelpMascot from './ContextualHelpMascot'; +import githubService from '../services/githubService'; +import './RequirementsEditor.css'; + +/** + * RequirementsEditor Component + * + * Editor for WHO SMART Guidelines Functional and Non-Functional Requirements + * Based on the WHO smart-base logical models: + * - FunctionalRequirement: https://worldhealthorganization.github.io/smart-base/StructureDefinition-FunctionalRequirement.html + * - NonFunctionalRequirement: https://worldhealthorganization.github.io/smart-base/StructureDefinition-NonFunctionalRequirement.html + * + * Supports creating, editing, and deleting FSH files for requirements. + */ + +const RequirementsEditor = () => { + return ( + + + + ); +}; + +const RequirementsEditorContent = () => { + const { repository, branch, isLoading: pageLoading } = useDAKParams(); + + // Component state + const [requirements, setRequirements] = useState([]); + const [selectedRequirement, setSelectedRequirement] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [editing, setEditing] = useState(false); + const [requirementContent, setRequirementContent] = useState(null); + const [requirementType, setRequirementType] = useState('functional'); // 'functional' or 'nonfunctional' + const [showCreateNew, setShowCreateNew] = useState(false); + + // Extract user and repo from repository + const user = repository?.owner?.login || repository?.full_name?.split('/')[0]; + const repo = repository?.name || repository?.full_name?.split('/')[1]; + + // Fetch requirements FSH files from input/fsh/requirements directory + useEffect(() => { + const fetchRequirements = async () => { + if (!user || !repo || !branch) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Try to fetch the input/fsh/requirements directory + let requirementFiles = []; + try { + const fshRequirementsContents = await githubService.getDirectoryContents( + user, + repo, + 'input/fsh/requirements', + branch + ); + + // Filter for .fsh files + requirementFiles = fshRequirementsContents + .filter(file => file.name.endsWith('.fsh') && file.type === 'file') + .map(file => ({ + name: file.name, + path: file.path, + download_url: file.download_url, + html_url: file.html_url, + sha: file.sha + })); + } catch (err) { + if (err.status !== 404) { + throw err; + } + // Directory doesn't exist yet - that's OK + } + + setRequirements(requirementFiles); + } catch (err) { + console.error('Error fetching requirements:', err); + setError('Failed to load requirements files'); + } finally { + setLoading(false); + } + }; + + fetchRequirements(); + }, [user, repo, branch]); + + // Load requirement content when selected + const handleRequirementSelect = async (requirement) => { + setSelectedRequirement(requirement); + setEditing(true); + setShowCreateNew(false); + + try { + const response = await fetch(requirement.download_url); + const content = await response.text(); + + setRequirementContent(content); + + // Detect type from filename or content + if (requirement.name.toLowerCase().includes('nonfunctional') || + requirement.name.toLowerCase().includes('non-functional')) { + setRequirementType('nonfunctional'); + } else { + setRequirementType('functional'); + } + } catch (err) { + console.error('Error loading requirement:', err); + setError('Failed to load requirement content'); + } + }; + + // Create new requirement + const handleCreateNew = (type) => { + setRequirementType(type); + setShowCreateNew(true); + setEditing(true); + setSelectedRequirement(null); + + const template = type === 'functional' + ? generateFunctionalRequirementTemplate() + : generateNonFunctionalRequirementTemplate(); + + setRequirementContent(template); + }; + + // Save requirement + const handleSave = async () => { + if (!requirementContent) return; + + try { + const fileName = showCreateNew + ? `${requirementType === 'functional' ? 'Functional' : 'NonFunctional'}Requirement-${Date.now()}.fsh` + : selectedRequirement.name; + + const filePath = `input/fsh/requirements/${fileName}`; + const commitMessage = showCreateNew + ? `Add ${requirementType} requirement: ${fileName}` + : `Update ${requirementType} requirement: ${fileName}`; + + await githubService.createOrUpdateFile( + user, + repo, + filePath, + requirementContent, + commitMessage, + branch, + showCreateNew ? null : selectedRequirement.sha + ); + + // Refresh requirements list + const updatedRequirements = [...requirements]; + if (showCreateNew) { + updatedRequirements.push({ + name: fileName, + path: filePath, + download_url: `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${filePath}`, + html_url: `https://github.com/${user}/${repo}/blob/${branch}/${filePath}` + }); + } + setRequirements(updatedRequirements); + + // Reset state + setEditing(false); + setShowCreateNew(false); + setSelectedRequirement(null); + setRequirementContent(null); + + } catch (err) { + console.error('Error saving requirement:', err); + setError('Failed to save requirement'); + } + }; + + // Cancel editing + const handleCancel = () => { + setEditing(false); + setShowCreateNew(false); + setSelectedRequirement(null); + setRequirementContent(null); + setRequirementType('functional'); + }; + + // Delete requirement + const handleDelete = async () => { + if (!selectedRequirement) return; + + const confirmDelete = window.confirm( + `Are you sure you want to delete ${selectedRequirement.name}?` + ); + + if (!confirmDelete) return; + + try { + await githubService.deleteFile( + user, + repo, + selectedRequirement.path, + `Delete requirement: ${selectedRequirement.name}`, + branch, + selectedRequirement.sha + ); + + // Remove from list + setRequirements(requirements.filter(r => r.name !== selectedRequirement.name)); + + // Reset state + setEditing(false); + setSelectedRequirement(null); + setRequirementContent(null); + } catch (err) { + console.error('Error deleting requirement:', err); + setError('Failed to delete requirement'); + } + }; + + if (pageLoading || loading) { + return ( +
+
+

Loading requirements...

+
+ ); + } + + return ( + +
+ + + {error && ( +
+ ⚠️ + {error} +
+ )} + +
+ {/* Left sidebar - Requirements list */} +
+
+

Requirements ({requirements.length})

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

No requirements found.

+

Create a new functional or non-functional requirement to get started.

+
+ ) : ( + requirements.map(req => ( +
handleRequirementSelect(req)} + onKeyPress={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleRequirementSelect(req); + } + }} + role="button" + tabIndex={0} + aria-label={`Select requirement ${req.name}`} + > +
+ {req.name.toLowerCase().includes('nonfunctional') || + req.name.toLowerCase().includes('non-functional') ? 'πŸ“‹' : 'βš™οΈ'} +
+
{req.name}
+
+ )) + )} +
+
+ + {/* Right panel - Editor */} +
+ {!editing ? ( +
+

Requirements Editor

+

+ Select a requirement from the list or create a new one to get started. +

+
+

About Requirements

+

+ Requirements define the system capabilities and constraints for a DAK implementation. +

+
    +
  • Functional Requirements: Define what the system must do (capabilities, features, behaviors)
  • +
  • Non-Functional Requirements: Define how the system should perform (performance, security, usability)
  • +
+

+ Requirements are stored as FSH (FHIR Shorthand) files in input/fsh/requirements/. +

+
+
+ ) : ( +
+
+

+ {showCreateNew ? 'New ' : 'Edit '} + {requirementType === 'functional' ? 'Functional' : 'Non-Functional'} Requirement +

+
+ {!showCreateNew && ( + + )} + + +
+
+ +
+