+ )}
);
@@ -417,42 +722,23 @@ function generateLogicalModelHeader(id, title, description, parent) {
return lines.join('\n');
}
-// Generate template for functional requirement based on WHO smart-base model
-function generateFunctionalRequirementTemplate() {
- const header = generateLogicalModelHeader(
- 'NewFunctionalRequirement',
- 'New Functional Requirement',
- 'Description of the functional requirement',
- 'FunctionalRequirement'
- );
+// Helper function to parse FSH field values
+function parseFSHValue(fshContent, fieldName) {
+ // Pattern to match FSH field assignments
+ const patterns = [
+ new RegExp(`\\*\\s*${fieldName}\\s*=\\s*"([^"]*)"`, 'i'), // * field = "value"
+ new RegExp(`\\*\\s*${fieldName}\\s*=\\s*#(\\S+)`, 'i'), // * field = #code
+ new RegExp(`\\*\\s*${fieldName}\\s*=\\s*Reference\\(([^)]+)\\)`, 'i') // * field = Reference(Actor)
+ ];
- return `${header}
-
-* id = "FR-NEW-001"
-* activity = "${escapeFSHString('Description of the activity being performed')}"
-* actor = Reference(ActorName) // Optional: Reference to actor
-* capability = "${escapeFSHString('I want to...')}" // Optional: Capability statement
-* benefit = "${escapeFSHString('So that...')}" // Optional: Benefit statement
-* classification = #category // Optional: Classification code
-`;
-}
-
-// Generate template for non-functional requirement based on WHO smart-base model
-function generateNonFunctionalRequirementTemplate() {
- const header = generateLogicalModelHeader(
- 'NewNonFunctionalRequirement',
- 'New Non-Functional Requirement',
- 'Description of the non-functional requirement',
- 'NonFunctionalRequirement'
- );
+ for (const pattern of patterns) {
+ const match = fshContent.match(pattern);
+ if (match && match[1]) {
+ return match[1];
+ }
+ }
- return `${header}
-
-* id = "NFR-NEW-001"
-* requirement = "${escapeFSHString('Description of the non-functional requirement')}"
-* category = #performance // Optional: Category (e.g., #performance, #security, #usability)
-* classification = #category // Optional: Classification code
-`;
+ return '';
}
export default RequirementsEditor;
From e9c2e5811447ca81d594eae322e0a0505c936b40 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 00:32:08 +0000
Subject: [PATCH 08/16] Fix duplicate SGEX title bar by removing
AssetEditorLayout wrapper
The component was using both PageLayout (outer wrapper) and AssetEditorLayout (inner wrapper). Since AssetEditorLayout internally creates its own PageLayout, this caused a duplicate title bar. Removed AssetEditorLayout and now only using PageLayout like ActorEditor does.
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 26 +++++++++++---------------
1 file changed, 11 insertions(+), 15 deletions(-)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index 536ab3d572..a7d9070d45 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { PageLayout, AssetEditorLayout, useDAKParams } from './framework';
+import { PageLayout, useDAKParams } from './framework';
import ContextualHelpMascot from './ContextualHelpMascot';
import githubService from '../services/githubService';
import { escapeFSHString, extractFSHMetadata } from '@sgex/dak-core/dist/browser';
@@ -355,19 +355,15 @@ const RequirementsEditorContent = () => {
}
return (
-
-
-
-
- {error && (
-
- β οΈ
- {error}
-
- )}
+
+
+
+ {error && (
+
+ β οΈ
+ {error}
+
+ )}
{/* Left sidebar - Requirements list */}
@@ -705,7 +701,7 @@ const RequirementsEditorContent = () => {
)}
-
+
);
};
From 179e91b9414f74723073ac24258c2d2704d0ff28 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 00:43:01 +0000
Subject: [PATCH 09/16] Fix JSX syntax error - correct indentation of
requirements-layout div
Fixed the indentation of the requirements-layout div which was causing "Adjacent JSX elements must be wrapped" error. The div was indented incorrectly making it appear as a sibling element instead of a child of requirements-editor div.
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index a7d9070d45..d3713285af 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -365,9 +365,9 @@ const RequirementsEditorContent = () => {
)}
-
- {/* Left sidebar - Requirements list */}
-
+
+ {/* Left sidebar - Requirements list */}
+
Requirements ({requirements.length})
From fdef89a189a9e316e6aada9ae8688a0cda86b718 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 16:10:05 +0000
Subject: [PATCH 10/16] Fix JSX structure - remove extra closing div tag and
unused state reference
- Removed extra closing div tag that was causing JSX syntax error
- Removed unused setRequirementContent reference from handleDelete function
- Build now completes successfully (CSS ordering warning is non-fatal)
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index d3713285af..a11ec014d0 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -338,7 +338,6 @@ const RequirementsEditorContent = () => {
// Reset state
setEditing(false);
setSelectedRequirement(null);
- setRequirementContent(null);
} catch (err) {
console.error('Error deleting requirement:', err);
setError('Failed to delete requirement');
@@ -701,7 +700,6 @@ const RequirementsEditorContent = () => {
)}
-
);
};
From 866669a6ecd1e970d599557523b1bc6762fecff5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:12:30 +0000
Subject: [PATCH 11/16] Add WHO IG Starter Kit ID validation and staging ground
integration
- Created requirementsService.js for requirements management
- Implements WHO IG Starter Kit ID naming conventions:
* IDs MUST start with capital letter (e.g., "Resourceid", "Resource-id")
* IDs MAY contain hyphens (although not preferred)
* IDs SHALL NOT contain underscores ("Resource_id" is invalid)
- Added real-time ID validation with helpful error messages in UI
- Integrated with stagingGroundService for DAK JSON updates
- Requirements automatically saved to staging ground when saved
- Added CSS styles for validation error states (input-error, error-text)
- Updated RequirementsEditor to use requirementsService
- Follows ActorDefinitionService pattern for consistency
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.css | 18 ++
src/components/RequirementsEditor.js | 58 ++++-
src/services/requirementsService.js | 298 ++++++++++++++++++++++++++
3 files changed, 366 insertions(+), 8 deletions(-)
create mode 100644 src/services/requirementsService.js
diff --git a/src/components/RequirementsEditor.css b/src/components/RequirementsEditor.css
index b6787cdecb..1d167565a6 100644
--- a/src/components/RequirementsEditor.css
+++ b/src/components/RequirementsEditor.css
@@ -328,6 +328,24 @@
font-style: italic;
}
+.error-text {
+ display: block;
+ margin-top: 0.25rem;
+ font-size: 0.85rem;
+ color: #d32f2f;
+ font-weight: 500;
+}
+
+.input-error {
+ border-color: #d32f2f !important;
+ background-color: #ffebee !important;
+}
+
+.input-error:focus {
+ border-color: #c62828 !important;
+ box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1) !important;
+}
+
.btn-preview {
background: #6b69d6;
color: white;
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index a11ec014d0..92b6a303ec 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { PageLayout, useDAKParams } from './framework';
import ContextualHelpMascot from './ContextualHelpMascot';
import githubService from '../services/githubService';
+import requirementsService from '../services/requirementsService';
import { escapeFSHString, extractFSHMetadata } from '@sgex/dak-core/dist/browser';
import './RequirementsEditor.css';
@@ -37,6 +38,7 @@ const RequirementsEditorContent = () => {
const [showCreateNew, setShowCreateNew] = useState(false);
const [showFSHPreview, setShowFSHPreview] = useState(false);
const [fshPreview, setFSHPreview] = useState('');
+ const [idValidationError, setIdValidationError] = useState(null);
// Form data for functional requirement
const [functionalReq, setFunctionalReq] = useState({
@@ -60,6 +62,23 @@ const RequirementsEditorContent = () => {
classification: ''
});
+ // Handle ID change with validation
+ const handleIdChange = (newId, isFunctional) => {
+ if (isFunctional) {
+ setFunctionalReq({...functionalReq, id: newId});
+ } else {
+ setNonFunctionalReq({...nonFunctionalReq, id: newId});
+ }
+
+ // Validate ID in real-time
+ if (newId) {
+ const validation = requirementsService.validateRequirementId(newId);
+ setIdValidationError(validation.isValid ? null : validation.error);
+ } else {
+ setIdValidationError(null);
+ }
+ };
+
// 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];
@@ -257,8 +276,19 @@ const RequirementsEditorContent = () => {
return;
}
+ // Validate ID according to WHO IG Starter Kit naming conventions
+ const idValidation = requirementsService.validateRequirementId(currentReq.id);
+ if (!idValidation.isValid) {
+ setError(idValidation.error);
+ return;
+ }
+
try {
- const fshContent = generateFSHFromForm();
+ // Save to staging ground first
+ await requirementsService.saveToStagingGround(currentReq, requirementType);
+
+ // Generate FSH content
+ const fshContent = requirementsService.generateFSH(currentReq, requirementType);
const fileName = showCreateNew
? `${requirementType === 'functional' ? 'Functional' : 'NonFunctional'}Requirement-${currentReq.id}.fsh`
@@ -296,10 +326,11 @@ const RequirementsEditorContent = () => {
setShowCreateNew(false);
setSelectedRequirement(null);
setError(null);
+ setIdValidationError(null);
} catch (err) {
console.error('Error saving requirement:', err);
- setError('Failed to save requirement');
+ setError(err.message || 'Failed to save requirement');
}
};
@@ -310,6 +341,7 @@ const RequirementsEditorContent = () => {
setSelectedRequirement(null);
setRequirementType('functional');
setError(null);
+ setIdValidationError(null);
};
// Delete requirement
@@ -475,11 +507,16 @@ const RequirementsEditorContent = () => {
type="text"
id="req-id"
value={functionalReq.id}
- onChange={(e) => setFunctionalReq({...functionalReq, id: e.target.value})}
- placeholder="e.g., FR-001"
+ onChange={(e) => handleIdChange(e.target.value, true)}
+ placeholder="e.g., FunctionalReq001 or Functional-Req-001"
required
+ className={idValidationError ? 'input-error' : ''}
/>
- Unique identifier for this requirement
+ {idValidationError ? (
+ {idValidationError}
+ ) : (
+ Must start with capital letter, no underscores. Hyphens allowed but not preferred.
+ )}
@@ -576,11 +613,16 @@ const RequirementsEditorContent = () => {
type="text"
id="nfr-id"
value={nonFunctionalReq.id}
- onChange={(e) => setNonFunctionalReq({...nonFunctionalReq, id: e.target.value})}
- placeholder="e.g., NFR-001"
+ onChange={(e) => handleIdChange(e.target.value, false)}
+ placeholder="e.g., NonFunctionalReq001 or NonFunctional-Req-001"
required
+ className={idValidationError ? 'input-error' : ''}
/>
- Unique identifier for this requirement
+ {idValidationError ? (
+ {idValidationError}
+ ) : (
+ Must start with capital letter, no underscores. Hyphens allowed but not preferred.
+ )}
diff --git a/src/services/requirementsService.js b/src/services/requirementsService.js
new file mode 100644
index 0000000000..34d8c6e4cf
--- /dev/null
+++ b/src/services/requirementsService.js
@@ -0,0 +1,298 @@
+/**
+ * Requirements Service
+ *
+ * Manages functional and non-functional requirements based on WHO smart-base logical models.
+ * Handles conversion to FSH (FHIR Shorthand) format and integration with staging ground.
+ *
+ * Based on:
+ * - FunctionalRequirement: https://worldhealthorganization.github.io/smart-base/StructureDefinition-FunctionalRequirement.html
+ * - NonFunctionalRequirement: https://worldhealthorganization.github.io/smart-base/StructureDefinition-NonFunctionalRequirement.html
+ */
+
+import stagingGroundService from './stagingGroundService';
+import { escapeFSHString, extractFSHMetadata } from '@sgex/dak-core/dist/browser';
+
+class RequirementsService {
+ /**
+ * Validate requirement ID according to WHO IG Starter Kit naming conventions
+ * https://smart.who.int/ig-starter-kit/authoring_conventions.html#naming-conventions
+ *
+ * Rules:
+ * - IDs MUST start with a capital letter
+ * - IDs MAY contain hyphens (although not preferred)
+ * - IDs SHALL NOT contain underscores
+ *
+ * @param {string} id - The requirement ID to validate
+ * @returns {object} - { isValid: boolean, error: string|null }
+ */
+ validateRequirementId(id) {
+ if (!id || typeof id !== 'string') {
+ return {
+ isValid: false,
+ error: 'ID is required'
+ };
+ }
+
+ // Check if ID starts with capital letter
+ if (!/^[A-Z]/.test(id)) {
+ return {
+ isValid: false,
+ error: 'ID must start with a capital letter (e.g., "Resourceid" or "Resource-id")'
+ };
+ }
+
+ // Check for underscores (not allowed)
+ if (id.includes('_')) {
+ return {
+ isValid: false,
+ error: 'ID SHALL NOT contain underscores. Use "Resource-id" instead of "Resource_id"'
+ };
+ }
+
+ // Check for valid characters (letters, numbers, hyphens)
+ if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(id)) {
+ return {
+ isValid: false,
+ error: 'ID may only contain letters, numbers, and hyphens'
+ };
+ }
+
+ return { isValid: true, error: null };
+ }
+
+ /**
+ * Generate FSH Logical Model header
+ */
+ generateLogicalModelHeader(id, title, description, parentType) {
+ const escapedTitle = escapeFSHString(title);
+ const escapedDescription = escapeFSHString(description);
+
+ return `Logical: ${id}
+Parent: ${parentType}
+Id: ${id}
+Title: "${escapedTitle}"
+Description: "${escapedDescription}"`;
+ }
+
+ /**
+ * Generate FSH content for functional requirement
+ */
+ generateFunctionalRequirementFSH(requirement) {
+ const { id, title, description, activity, actor, capability, benefit, classification } = requirement;
+
+ if (!id) {
+ throw new Error('Requirement ID is required');
+ }
+
+ // Validate ID
+ const idValidation = this.validateRequirementId(id);
+ if (!idValidation.isValid) {
+ throw new Error(`Invalid ID: ${idValidation.error}`);
+ }
+
+ const header = this.generateLogicalModelHeader(
+ id,
+ title || 'Functional Requirement',
+ description || 'Description of the functional requirement',
+ 'FunctionalRequirement'
+ );
+
+ const fields = [];
+ fields.push(`* id = "${id}"`);
+ if (activity) fields.push(`* activity = "${escapeFSHString(activity)}"`);
+ if (actor) fields.push(`* actor = Reference(${actor})`);
+ if (capability) fields.push(`* capability = "${escapeFSHString(capability)}"`);
+ if (benefit) fields.push(`* benefit = "${escapeFSHString(benefit)}"`);
+ if (classification) fields.push(`* classification = #${classification}`);
+
+ return `${header}\n\n${fields.join('\n')}\n`;
+ }
+
+ /**
+ * Generate FSH content for non-functional requirement
+ */
+ generateNonFunctionalRequirementFSH(requirement) {
+ const { id, title, description, requirement: reqText, category, classification } = requirement;
+
+ if (!id) {
+ throw new Error('Requirement ID is required');
+ }
+
+ // Validate ID
+ const idValidation = this.validateRequirementId(id);
+ if (!idValidation.isValid) {
+ throw new Error(`Invalid ID: ${idValidation.error}`);
+ }
+
+ const header = this.generateLogicalModelHeader(
+ id,
+ title || 'Non-Functional Requirement',
+ description || 'Description of the non-functional requirement',
+ 'NonFunctionalRequirement'
+ );
+
+ const fields = [];
+ fields.push(`* id = "${id}"`);
+ if (reqText) fields.push(`* requirement = "${escapeFSHString(reqText)}"`);
+ if (category) fields.push(`* category = #${category}`);
+ if (classification) fields.push(`* classification = #${classification}`);
+
+ return `${header}\n\n${fields.join('\n')}\n`;
+ }
+
+ /**
+ * Generate FSH for any requirement type
+ */
+ generateFSH(requirement, type) {
+ if (type === 'functional') {
+ return this.generateFunctionalRequirementFSH(requirement);
+ } else if (type === 'nonfunctional') {
+ return this.generateNonFunctionalRequirementFSH(requirement);
+ } else {
+ throw new Error(`Unknown requirement type: ${type}`);
+ }
+ }
+
+ /**
+ * Validate requirement object
+ */
+ validateRequirement(requirement, type) {
+ const errors = [];
+
+ if (!requirement.id) {
+ errors.push('ID is required');
+ } else {
+ const idValidation = this.validateRequirementId(requirement.id);
+ if (!idValidation.isValid) {
+ errors.push(idValidation.error);
+ }
+ }
+
+ if (type === 'functional') {
+ if (!requirement.activity) {
+ errors.push('Activity is required for functional requirements');
+ }
+ } else if (type === 'nonfunctional') {
+ if (!requirement.requirement) {
+ errors.push('Requirement text is required for non-functional requirements');
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors: errors
+ };
+ }
+
+ /**
+ * Save requirement to staging ground
+ * Integrates with the DAK JSON object for centralized change management
+ */
+ async saveToStagingGround(requirement, type) {
+ try {
+ // Validate first
+ const validation = this.validateRequirement(requirement, type);
+ if (!validation.isValid) {
+ throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
+ }
+
+ // Generate FSH content
+ const fshContent = this.generateFSH(requirement, type);
+
+ // Create file path in staging ground structure
+ const prefix = type === 'functional' ? 'Functional' : 'NonFunctional';
+ const filePath = `input/fsh/requirements/${prefix}Requirement-${requirement.id}.fsh`;
+
+ // Save to staging ground with metadata
+ const success = stagingGroundService.updateFile(filePath, fshContent, {
+ type: `${type}-requirement`,
+ requirementId: requirement.id,
+ requirementTitle: requirement.title,
+ requirementType: type,
+ lastModified: Date.now(),
+ source: 'requirements-editor'
+ });
+
+ if (!success) {
+ throw new Error('Failed to save to staging ground');
+ }
+
+ return {
+ success: true,
+ filePath: filePath
+ };
+ } catch (error) {
+ console.error('Error saving requirement to staging ground:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Parse FSH content to extract requirement data
+ */
+ parseFSH(fshContent) {
+ try {
+ const metadata = extractFSHMetadata(fshContent);
+ const lines = fshContent.split('\n');
+
+ const requirement = {
+ id: metadata.id || '',
+ title: metadata.title || '',
+ description: metadata.description || ''
+ };
+
+ // Parse field values
+ lines.forEach(line => {
+ const trimmed = line.trim();
+ if (trimmed.startsWith('* activity =')) {
+ requirement.activity = this.extractQuotedValue(trimmed);
+ } else if (trimmed.startsWith('* actor =')) {
+ requirement.actor = this.extractReferenceValue(trimmed);
+ } else if (trimmed.startsWith('* capability =')) {
+ requirement.capability = this.extractQuotedValue(trimmed);
+ } else if (trimmed.startsWith('* benefit =')) {
+ requirement.benefit = this.extractQuotedValue(trimmed);
+ } else if (trimmed.startsWith('* requirement =')) {
+ requirement.requirement = this.extractQuotedValue(trimmed);
+ } else if (trimmed.startsWith('* category =')) {
+ requirement.category = this.extractCodeValue(trimmed);
+ } else if (trimmed.startsWith('* classification =')) {
+ requirement.classification = this.extractCodeValue(trimmed);
+ }
+ });
+
+ return requirement;
+ } catch (error) {
+ console.error('Error parsing FSH:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Extract quoted string value from FSH line
+ */
+ extractQuotedValue(line) {
+ const match = line.match(/=\s*"(.*)"/);
+ return match ? match[1] : '';
+ }
+
+ /**
+ * Extract code value (after #) from FSH line
+ */
+ extractCodeValue(line) {
+ const match = line.match(/=\s*#(\S+)/);
+ return match ? match[1] : '';
+ }
+
+ /**
+ * Extract reference value from FSH line
+ */
+ extractReferenceValue(line) {
+ const match = line.match(/=\s*Reference\(([^)]+)\)/);
+ return match ? match[1] : '';
+ }
+}
+
+// Export singleton instance
+const requirementsService = new RequirementsService();
+export default requirementsService;
From c0f7611f902627657414322d52384b0b235458bf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Oct 2025 16:34:17 +0000
Subject: [PATCH 12/16] Fix staging ground initialization error in
RequirementsEditor
- Import stagingGroundService at component level
- Initialize staging ground with repository and branch on component mount
- Fixes "Staging ground not initialized" error when saving requirements
- Follows same pattern as ActorEditor for staging ground integration
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index 92b6a303ec..a8510dde8b 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -3,6 +3,7 @@ import { PageLayout, useDAKParams } from './framework';
import ContextualHelpMascot from './ContextualHelpMascot';
import githubService from '../services/githubService';
import requirementsService from '../services/requirementsService';
+import stagingGroundService from '../services/stagingGroundService';
import { escapeFSHString, extractFSHMetadata } from '@sgex/dak-core/dist/browser';
import './RequirementsEditor.css';
@@ -83,6 +84,17 @@ const RequirementsEditorContent = () => {
const user = repository?.owner?.login || repository?.full_name?.split('/')[0];
const repo = repository?.name || repository?.full_name?.split('/')[1];
+ // Initialize staging ground when repository and branch are available
+ useEffect(() => {
+ if (repository && branch) {
+ try {
+ stagingGroundService.initialize(repository, branch);
+ } catch (error) {
+ console.error('Error initializing staging ground:', error);
+ }
+ }
+ }, [repository, branch]);
+
// Fetch requirements FSH files from input/fsh/requirements directory
useEffect(() => {
const fetchRequirements = async () => {
From bfd509195c2be2ffd121d1535a515d821825337a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Oct 2025 19:47:46 +0000
Subject: [PATCH 13/16] Fix githubService method call - use updateFile instead
of createOrUpdateFile
The githubService doesn't have a createOrUpdateFile method. Changed to use updateFile which handles both create and update operations via the GitHub API's createOrUpdateFileContents endpoint.
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index a8510dde8b..b26b6a0f99 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -311,14 +311,13 @@ const RequirementsEditorContent = () => {
? `Add ${requirementType} requirement: ${currentReq.id}`
: `Update ${requirementType} requirement: ${currentReq.id}`;
- await githubService.createOrUpdateFile(
+ await githubService.updateFile(
user,
repo,
filePath,
fshContent,
commitMessage,
- branch,
- showCreateNew ? null : selectedRequirement.sha
+ branch
);
// Refresh requirements list
From 613036aecdba4817c77c13e59ab47804b0c2f637 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 10 Oct 2025 20:13:02 +0000
Subject: [PATCH 14/16] Fix staging ground save - use synchronous updateFile,
not GitHub API
The saveToStagingGround method was incorrectly trying to call GitHub API which caused 404 errors. Changed to properly use stagingGroundService.updateFile() synchronously, which stores files locally for later commit via staging ground workflow.
- Removed async/await from saveToStagingGround
- Removed GitHub API parameters (repository, owner, branch, token)
- Made handleSave synchronous to match
- Files now properly saved to local staging ground cache
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.js | 42 ++++------------------------
src/services/requirementsService.js | 3 +-
2 files changed, 8 insertions(+), 37 deletions(-)
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index b26b6a0f99..267b12e7da 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -280,7 +280,7 @@ const RequirementsEditorContent = () => {
};
// Save requirement
- const handleSave = async () => {
+ const handleSave = () => {
const currentReq = requirementType === 'functional' ? functionalReq : nonFunctionalReq;
if (!currentReq.id) {
@@ -296,41 +296,11 @@ const RequirementsEditorContent = () => {
}
try {
- // Save to staging ground first
- await requirementsService.saveToStagingGround(currentReq, requirementType);
+ // Save to staging ground - this handles FSH generation and staging
+ requirementsService.saveToStagingGround(currentReq, requirementType);
- // Generate FSH content
- const fshContent = requirementsService.generateFSH(currentReq, requirementType);
-
- const fileName = showCreateNew
- ? `${requirementType === 'functional' ? 'Functional' : 'NonFunctional'}Requirement-${currentReq.id}.fsh`
- : selectedRequirement.name;
-
- const filePath = `input/fsh/requirements/${fileName}`;
- const commitMessage = showCreateNew
- ? `Add ${requirementType} requirement: ${currentReq.id}`
- : `Update ${requirementType} requirement: ${currentReq.id}`;
-
- await githubService.updateFile(
- user,
- repo,
- filePath,
- fshContent,
- commitMessage,
- branch
- );
-
- // 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);
+ // Show success message
+ alert(`Requirement ${currentReq.id} saved to staging ground successfully! Use the staging ground to commit your changes.`);
// Reset state
setEditing(false);
@@ -341,7 +311,7 @@ const RequirementsEditorContent = () => {
} catch (err) {
console.error('Error saving requirement:', err);
- setError(err.message || 'Failed to save requirement');
+ setError(err.message || 'Failed to save requirement to staging ground');
}
};
diff --git a/src/services/requirementsService.js b/src/services/requirementsService.js
index 34d8c6e4cf..f2e5987571 100644
--- a/src/services/requirementsService.js
+++ b/src/services/requirementsService.js
@@ -188,7 +188,7 @@ Description: "${escapedDescription}"`;
* Save requirement to staging ground
* Integrates with the DAK JSON object for centralized change management
*/
- async saveToStagingGround(requirement, type) {
+ saveToStagingGround(requirement, type) {
try {
// Validate first
const validation = this.validateRequirement(requirement, type);
@@ -204,6 +204,7 @@ Description: "${escapedDescription}"`;
const filePath = `input/fsh/requirements/${prefix}Requirement-${requirement.id}.fsh`;
// Save to staging ground with metadata
+ // updateFile is synchronous and returns boolean
const success = stagingGroundService.updateFile(filePath, fshContent, {
type: `${type}-requirement`,
requirementId: requirement.id,
From 68d701f8502daa6b44825bc217cbf2f493a8af26 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Oct 2025 11:57:02 +0000
Subject: [PATCH 15/16] Replace browser alerts with React-based notification
system
- Removed window.alert() calls, using React state for success/error messages
- Replaced window.confirm() with custom confirmation dialog component
- Added success-message styling to match error-message
- Added confirm-dialog styles with overlay
- Success messages auto-dismiss after 5 seconds
- Delete confirmation shows custom modal instead of browser alert
- Better UX with styled, consistent notification system
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
src/components/RequirementsEditor.css | 66 +++++++++++++++++++++
src/components/RequirementsEditor.js | 83 +++++++++++++++++++++------
2 files changed, 131 insertions(+), 18 deletions(-)
diff --git a/src/components/RequirementsEditor.css b/src/components/RequirementsEditor.css
index 1d167565a6..8d4d03258e 100644
--- a/src/components/RequirementsEditor.css
+++ b/src/components/RequirementsEditor.css
@@ -42,6 +42,72 @@
font-size: 1.5rem;
}
+.success-message {
+ background: #e8f5e9;
+ border: 1px solid #81c784;
+ border-radius: 0.5rem;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ color: #2e7d32;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.success-icon {
+ font-size: 1.5rem;
+}
+
+.confirm-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.confirm-dialog {
+ background: white;
+ border-radius: 0.5rem;
+ padding: 2rem;
+ max-width: 400px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.confirm-dialog h3 {
+ margin-top: 0;
+ color: #c62828;
+}
+
+.confirm-dialog-buttons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1.5rem;
+ justify-content: flex-end;
+}
+
+.confirm-dialog-buttons button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 0.25rem;
+ cursor: pointer;
+}
+
+.confirm-dialog-buttons .btn-cancel {
+ background: #e0e0e0;
+ color: #333;
+}
+
+.confirm-dialog-buttons .btn-delete {
+ background: #c62828;
+ color: white;
+}
+
/* Requirements Layout */
.requirements-layout {
display: grid;
diff --git a/src/components/RequirementsEditor.js b/src/components/RequirementsEditor.js
index 267b12e7da..a4eebf59b1 100644
--- a/src/components/RequirementsEditor.js
+++ b/src/components/RequirementsEditor.js
@@ -34,12 +34,14 @@ const RequirementsEditorContent = () => {
const [selectedRequirement, setSelectedRequirement] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
const [editing, setEditing] = useState(false);
const [requirementType, setRequirementType] = useState('functional'); // 'functional' or 'nonfunctional'
const [showCreateNew, setShowCreateNew] = useState(false);
const [showFSHPreview, setShowFSHPreview] = useState(false);
const [fshPreview, setFSHPreview] = useState('');
const [idValidationError, setIdValidationError] = useState(null);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Form data for functional requirement
const [functionalReq, setFunctionalReq] = useState({
@@ -297,18 +299,24 @@ const RequirementsEditorContent = () => {
try {
// Save to staging ground - this handles FSH generation and staging
- requirementsService.saveToStagingGround(currentReq, requirementType);
-
- // Show success message
- alert(`Requirement ${currentReq.id} saved to staging ground successfully! Use the staging ground to commit your changes.`);
-
- // Reset state
- setEditing(false);
- setShowCreateNew(false);
- setSelectedRequirement(null);
- setError(null);
- setIdValidationError(null);
+ const result = requirementsService.saveToStagingGround(currentReq, requirementType);
+ if (result.success) {
+ // Show success message
+ setSuccessMessage(`Requirement ${currentReq.id} saved to staging ground successfully! Use the staging ground to commit your changes.`);
+
+ // Reset state
+ setEditing(false);
+ setShowCreateNew(false);
+ setSelectedRequirement(null);
+ setError(null);
+ setIdValidationError(null);
+
+ // Clear success message after 5 seconds
+ setTimeout(() => setSuccessMessage(null), 5000);
+ } else {
+ setError('Failed to save requirement to staging ground');
+ }
} catch (err) {
console.error('Error saving requirement:', err);
setError(err.message || 'Failed to save requirement to staging ground');
@@ -328,12 +336,11 @@ const RequirementsEditorContent = () => {
// Delete requirement
const handleDelete = async () => {
if (!selectedRequirement) return;
-
- const confirmDelete = window.confirm(
- `Are you sure you want to delete ${selectedRequirement.name}?`
- );
-
- if (!confirmDelete) return;
+ setShowDeleteConfirm(true);
+ };
+
+ const confirmDelete = async () => {
+ if (!selectedRequirement) return;
try {
await githubService.deleteFile(
@@ -351,6 +358,19 @@ const RequirementsEditorContent = () => {
// Reset state
setEditing(false);
setSelectedRequirement(null);
+ setShowDeleteConfirm(false);
+ setSuccessMessage(`Requirement ${selectedRequirement.name} deleted successfully`);
+ setTimeout(() => setSuccessMessage(null), 5000);
+ } catch (err) {
+ console.error('Error deleting requirement:', err);
+ setError(err.message || 'Failed to delete requirement');
+ setShowDeleteConfirm(false);
+ }
+ };
+
+ const cancelDelete = () => {
+ setShowDeleteConfirm(false);
+ };
} catch (err) {
console.error('Error deleting requirement:', err);
setError('Failed to delete requirement');
@@ -377,6 +397,13 @@ const RequirementsEditorContent = () => {