From 51542197b31ef1ca9c5a0cb07da037d6d126da6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:02:25 +0000 Subject: [PATCH 1/4] Initial plan From 010959d2672d9242de8f8994b47ab980a81cfe8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:16:06 +0000 Subject: [PATCH 2/4] Fix missing deployment structure - add sgex/ to .gitignore and verify local deployment works Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com> --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 28842349b..77730a93f 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,7 @@ temp_serve/ # Generated QA reports (auto-generated by CI, should not be committed) docs/qa-report.html public/docs/qa-report.html -docs/github-issues-analysis.md \ No newline at end of file +docs/github-issues-analysis.md + +# Deployment structure (generated by CI/CD) +sgex/ \ No newline at end of file From 87ee122ba9592247bf9cff77d4527f5b3317deda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:47:30 +0000 Subject: [PATCH 3/4] Refactor: Remove deployment files from main branch - Removed scripts/build-multi-branch.js (moved to deploy branch) - Removed public/branch-listing.html (moved to deploy branch) - Removed src/components/BranchListing.js and CSS (moved to deploy branch) - Removed deployment-specific tests and services - Updated .github/workflows/pages.yml to fetch build script from deploy branch - Created new .github/workflows/landing-page-deploy.yml for deploy branch - Updated package.json to remove build:multi-branch script - Updated test deployment script to work with deploy branch separation Main branch now focuses purely on SGEX application code. Deployment infrastructure is isolated in deploy branch for better separation of concerns. --- .github/workflows/landing-page-deploy.yml | 25 +- .github/workflows/pages.yml | 7 + package.json | 1 - public/branch-listing.html | 2021 ----------------- scripts/build-multi-branch.js | 221 -- scripts/test-deployment.sh | 36 +- src/App.js | 2 - src/components/BranchListing.css | 1315 ----------- src/components/BranchListing.js | 1142 ---------- src/services/branchListingCacheService.js | 187 -- .../branchListingCacheService.test.js | 230 -- .../branchListingCacheService.test.js.bak | 254 --- src/tests/BranchListing.test.js | 208 -- src/tests/BranchListingCaching.test.js | 223 -- src/tests/BranchListingComments.test.js | 176 -- src/tests/BranchListingSearchByBranch.test.js | 173 -- src/tests/CommentErrorHandling.test.js | 278 --- src/tests/preview-url-linkable.test.js | 71 - 18 files changed, 42 insertions(+), 6528 deletions(-) delete mode 100644 public/branch-listing.html delete mode 100755 scripts/build-multi-branch.js delete mode 100644 src/components/BranchListing.css delete mode 100644 src/components/BranchListing.js delete mode 100644 src/services/branchListingCacheService.js delete mode 100644 src/services/branchListingCacheService.test.js delete mode 100644 src/services/branchListingCacheService.test.js.bak delete mode 100644 src/tests/BranchListing.test.js delete mode 100644 src/tests/BranchListingCaching.test.js delete mode 100644 src/tests/BranchListingComments.test.js delete mode 100644 src/tests/BranchListingSearchByBranch.test.js delete mode 100644 src/tests/CommentErrorHandling.test.js delete mode 100644 src/tests/preview-url-linkable.test.js diff --git a/.github/workflows/landing-page-deploy.yml b/.github/workflows/landing-page-deploy.yml index f49b86331..db7555c4b 100644 --- a/.github/workflows/landing-page-deploy.yml +++ b/.github/workflows/landing-page-deploy.yml @@ -11,9 +11,9 @@ on: workflow_dispatch: inputs: source_branch: - description: 'Branch to use for build scripts (defaults to current branch)' + description: 'Branch to use for build scripts (defaults to deploy branch)' required: false - default: '' + default: 'deploy' permissions: contents: write @@ -28,9 +28,10 @@ jobs: deploy-landing-page: runs-on: ubuntu-latest steps: - - name: Checkout repository + - name: Checkout deploy branch uses: actions/checkout@v4 with: + ref: ${{ github.event.inputs.source_branch || 'deploy' }} fetch-depth: 0 # Fetch full history for gh-pages branch - name: Configure git user @@ -53,17 +54,10 @@ jobs: run: | set -e - echo "Building self-contained landing page..." - # Use current branch for build scripts unless specified - source_branch="${{ github.event.inputs.source_branch }}" - if [[ -z "$source_branch" ]]; then - source_branch="${{ github.ref_name }}" - fi - - echo "Using build scripts from branch: $source_branch" + echo "Building self-contained landing page from deploy branch..." # Build the landing page with self-contained assets - node scripts/build-multi-branch.js landing + npm run build:landing # Verify index.html exists if [[ ! -f "build/index.html" ]]; then @@ -161,7 +155,7 @@ jobs: # Determine deployment type for commit message if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then deployment_type="manual" - trigger_info="Triggered from branch: ${{ github.ref_name }}" + trigger_info="Triggered from deploy branch" else deployment_type="automatic" trigger_info="Auto-triggered by push to main" @@ -169,7 +163,7 @@ jobs: git commit -m "🏠 Deploy landing page (${deployment_type}) - - Updated landing page with self-contained assets + - Updated landing page with self-contained assets from deploy branch - ${trigger_info} - Deployed at $(date -u '+%Y-%m-%d %H:%M:%S UTC') - Commit: ${{ github.sha }}" @@ -189,7 +183,7 @@ jobs: # Determine deployment type for output message if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then deployment_type="Manual" - trigger_info="Triggered from branch: ${{ github.ref_name }}" + trigger_info="Triggered from deploy branch" else deployment_type="Automatic" trigger_info="Auto-triggered by push to main" @@ -200,4 +194,5 @@ jobs: echo "- Deployment Type: $deployment_type" echo "- $trigger_info" echo "- Deployed at: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "- Source Branch: ${{ github.event.inputs.source_branch || 'deploy' }}" echo "- Commit: ${{ github.sha }}" \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9de6edc78..fc559f7c9 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -73,6 +73,13 @@ jobs: export BASE_PATH="/${safe_branch_name}/" fi echo "Building with BASE_PATH: $BASE_PATH" + + # Get build script from deploy branch + echo "Fetching build script from deploy branch..." + git fetch origin deploy + git checkout origin/deploy -- scripts/build-multi-branch.js + + # Run the build script node scripts/build-multi-branch.js branch env: CI: false diff --git a/package.json b/package.json index 79b890f96..3ffe2f430 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "scripts": { "start": "craco start", "build": "react-scripts build", - "build:multi-branch": "node scripts/build-multi-branch.js", "test": "react-scripts test", "eject": "react-scripts eject", "serve": "npm run build && cd build && python3 -m http.server 3000", diff --git a/public/branch-listing.html b/public/branch-listing.html deleted file mode 100644 index 640ab1bae..000000000 --- a/public/branch-listing.html +++ /dev/null @@ -1,2021 +0,0 @@ - - - - - - SGEX Branch & PR Previews - - - - - - -
- - - - \ No newline at end of file diff --git a/scripts/build-multi-branch.js b/scripts/build-multi-branch.js deleted file mode 100755 index 085d523fd..000000000 --- a/scripts/build-multi-branch.js +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env node - -/** - * Multi-branch build script for SGEX - * - * This script handles two different build scenarios: - * 1. Branch-specific builds (for deployment to subdirectories) - * 2. Root landing page build (for branch listing at gh-pages root) - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -// Get build mode from environment or command line -const buildMode = process.env.BUILD_MODE || process.argv[2] || 'branch'; -const branchName = process.env.GITHUB_REF_NAME || 'main'; -const basePath = process.env.BASE_PATH || process.argv[3] || null; - -console.log(`🚀 Starting ${buildMode} build for branch: ${branchName}`); - -function ensureDependencies() { - const nodeModulesPath = path.join(__dirname, '..', 'node_modules'); - if (!fs.existsSync(nodeModulesPath)) { - console.log('đŸ“Ļ Installing dependencies...'); - execSync('npm ci', { stdio: 'inherit' }); - } -} - -function createBranchSpecificBuild() { - console.log('đŸ“Ļ Building branch-specific React app...'); - - // Ensure dependencies are available - ensureDependencies(); - - // Update package.json homepage for branch-specific deployment - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const originalHomepage = packageJson.homepage; - - try { - // Set homepage based on branch and base path - let deploymentPath; - if (basePath) { - // For GitHub Pages, ensure the repository name is included in the path - // basePath comes in format like "/copilot-fix-418/" but needs to be "/sgex/copilot-fix-418/" - if (basePath.startsWith('/') && !basePath.startsWith('/sgex/')) { - deploymentPath = `/sgex${basePath}`; - } else { - deploymentPath = basePath; - } - packageJson.homepage = deploymentPath; - console.log(`🔧 Setting homepage to: ${deploymentPath}`); - } else { - // Default path structure: /sgex/main/ for main, /sgex/safe-branch-name/ for others - const safeBranchName = branchName === 'main' ? 'main' : branchName.replace(/[^a-zA-Z0-9._-]/g, '-'); - deploymentPath = `/sgex/${safeBranchName}/`; - packageJson.homepage = deploymentPath; - console.log(`🔧 Setting homepage to: ${deploymentPath}`); - } - - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Update manifest.json for subdirectory deployment - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup'); - - if (fs.existsSync(manifestPath)) { - // Backup original manifest.json - fs.copyFileSync(manifestPath, manifestBackupPath); - - // Update manifest.json paths for subdirectory deployment - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const originalManifest = { ...manifest }; - - // Update start_url for subdirectory deployment - manifest.start_url = deploymentPath; - - // Update icon paths for subdirectory deployment - if (manifest.icons && Array.isArray(manifest.icons)) { - manifest.icons = manifest.icons.map(icon => ({ - ...icon, - src: icon.src.startsWith('/') ? icon.src : `${deploymentPath}${icon.src}` - })); - } - - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - console.log(`🔧 Updated manifest.json for subdirectory deployment`); - } - - // Standard React build for the branch - execSync('npm run build', { stdio: 'inherit' }); - - console.log(`✅ Branch-specific build completed for: ${branchName}`); - - } finally { - // Always restore original package.json - packageJson.homepage = originalHomepage; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Restore original manifest.json if backup exists - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup'); - - if (fs.existsSync(manifestBackupPath)) { - fs.copyFileSync(manifestBackupPath, manifestPath); - fs.unlinkSync(manifestBackupPath); - console.log(`🔧 Restored original manifest.json`); - } - } -} - -function createRootLandingPageApp() { - console.log('🏠 Creating self-contained root landing page application...'); - - // Ensure dependencies are available - ensureDependencies(); - - // Create a temporary React app that only renders BranchListing with self-contained assets - const tempAppContent = ` -import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import BranchListing from './components/BranchListing'; -import './App.css'; - -function App() { - return ( - -
- -
-
- ); -} - -export default App; -`; - - // Backup original App.js - const appJsPath = path.join(__dirname, '..', 'src', 'App.js'); - const appJsBackupPath = path.join(__dirname, '..', 'src', 'App.js.backup'); - - if (fs.existsSync(appJsPath)) { - fs.copyFileSync(appJsPath, appJsBackupPath); - } - - try { - // Write temporary App.js for landing page - fs.writeFileSync(appJsPath, tempAppContent.trim()); - - // Update package.json homepage for root deployment - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const originalHomepage = packageJson.homepage; - - // For root landing page, we want it to be at the GitHub Pages root for this repository - packageJson.homepage = '/sgex/'; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Update manifest.json for root deployment - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup-landing'); - - if (fs.existsSync(manifestPath)) { - // Backup original manifest.json - fs.copyFileSync(manifestPath, manifestBackupPath); - - // Update manifest.json for root deployment - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - - // Update start_url for root deployment - manifest.start_url = '/sgex/'; - - // Update icon paths for root deployment - ensure they're self-contained - if (manifest.icons && Array.isArray(manifest.icons)) { - manifest.icons = manifest.icons.map(icon => ({ - ...icon, - src: icon.src.startsWith('/') ? `/sgex${icon.src}` : `/sgex/${icon.src}` - })); - } - - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - console.log(`🔧 Updated manifest.json for self-contained root deployment`); - } - - // Build the landing page app - execSync('npm run build', { stdio: 'inherit' }); - - // Restore original package.json - packageJson.homepage = originalHomepage; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - console.log('✅ Self-contained root landing page build completed'); - - } finally { - // Always restore original App.js - if (fs.existsSync(appJsBackupPath)) { - fs.copyFileSync(appJsBackupPath, appJsPath); - fs.unlinkSync(appJsBackupPath); - } - - // Restore original manifest.json if backup exists - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup-landing'); - - if (fs.existsSync(manifestBackupPath)) { - fs.copyFileSync(manifestBackupPath, manifestPath); - fs.unlinkSync(manifestBackupPath); - console.log(`🔧 Restored original manifest.json`); - } - } -} - -// Execute based on build mode -if (buildMode === 'root' || buildMode === 'landing') { - createRootLandingPageApp(); -} else { - createBranchSpecificBuild(); -} - -console.log(`🎉 Build process completed successfully!`); \ No newline at end of file diff --git a/scripts/test-deployment.sh b/scripts/test-deployment.sh index 15e66d253..a7d925bfc 100755 --- a/scripts/test-deployment.sh +++ b/scripts/test-deployment.sh @@ -69,6 +69,14 @@ echo "-------------------------" echo "Testing branch-specific build..." export GITHUB_REF_NAME="test-branch" + +# Fetch build script from deploy branch (if available) +if git show-ref --verify --quiet refs/remotes/origin/deploy; then + echo "Fetching build script from deploy branch..." + git fetch origin deploy + git checkout origin/deploy -- scripts/build-multi-branch.js +fi + if node scripts/build-multi-branch.js branch > /dev/null 2>&1; then echo "✅ Branch-specific build: Success" if [[ -f "build/index.html" ]]; then @@ -82,6 +90,12 @@ fi echo "" echo "Testing root landing page build..." + +# Fetch landing page from deploy branch (if available) +if git show-ref --verify --quiet refs/remotes/origin/deploy; then + git checkout origin/deploy -- public/branch-listing.html +fi + if node scripts/build-multi-branch.js root > /dev/null 2>&1; then echo "✅ Root landing page build: Success" if [[ -f "build/index.html" ]]; then @@ -123,24 +137,24 @@ echo "" echo "đŸ§Ē Test 4: Component validation" echo "------------------------------" -echo "Testing BranchListing component..." -if [[ -f "src/components/BranchListing.js" ]] && [[ -f "src/components/BranchListing.css" ]]; then - echo "✅ BranchListing component files exist" +echo "Testing deployment components..." +if [[ -f "public/branch-listing.html" ]] && [[ -f "public/sgex-mascot.png" ]]; then + echo "✅ Landing page assets exist" - # Check for key elements in component - if grep -q "GitHub API" src/components/BranchListing.js; then - echo "✅ Component includes GitHub API integration" + # Check for key elements in landing page + if grep -q "GitHub API" public/branch-listing.html; then + echo "✅ Landing page includes GitHub API integration" fi - if grep -q "branch-card" src/components/BranchListing.css; then - echo "✅ Component includes card styling" + if grep -q "branch-card" public/branch-listing.html; then + echo "✅ Landing page includes card styling" fi - if grep -q "safeName" src/components/BranchListing.js; then - echo "✅ Component handles safe branch names" + if grep -q "safeBranchName" public/branch-listing.html; then + echo "✅ Landing page handles safe branch names" fi else - echo "❌ BranchListing component files missing" + echo "âš ī¸ Landing page assets may need to be fetched from deploy branch" fi # Test 5: Workflow file validation diff --git a/src/App.js b/src/App.js index a1f81ec84..bbc21000e 100644 --- a/src/App.js +++ b/src/App.js @@ -27,7 +27,6 @@ import DAKDashboardWithFramework from './components/DAKDashboardWithFramework'; import DashboardRedirect from './components/DashboardRedirect'; import TestDocumentationPage from './components/TestDocumentationPage'; import AssetEditorTest from './components/AssetEditorTest'; -import BranchListing from './components/BranchListing'; import logger from './utils/logger'; import './App.css'; @@ -112,7 +111,6 @@ function App() { } /> } /> } /> - } /> } /> diff --git a/src/components/BranchListing.css b/src/components/BranchListing.css deleted file mode 100644 index 18d11cb07..000000000 --- a/src/components/BranchListing.css +++ /dev/null @@ -1,1315 +0,0 @@ -.branch-listing { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; -} - -.branch-listing-header { - text-align: center; - margin-bottom: 2rem; -} - -.branch-listing-header h1 { - font-size: 2.5rem; - color: #2c3e50; - margin-bottom: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; -} - -.sgex-icon { - width: 48px; - height: 48px; - object-fit: contain; -} - -.branch-listing-header .subtitle { - font-size: 1.2rem; - color: #666; - margin: 0 0 1rem 0; - font-style: italic; -} - -.prominent-info { - background: var(--who-secondary-bg, #f8f9fa); - border: 2px solid var(--who-blue, #0366d6); - border-radius: 12px; - padding: 1.5rem; - margin: 1.5rem 0; - text-align: center; -} - -.info-text { - font-size: 1.1rem; - font-weight: 600; - color: var(--who-text-primary, #333); - margin: 0; -} - -.cache-status { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--who-border, #e1e4e8); -} - -.cache-info { - font-size: 0.9rem; - color: var(--who-text-secondary, #666); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.refresh-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - background: var(--who-blue, #0366d6); - color: white; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.refresh-btn:hover:not(:disabled) { - background: var(--who-blue-dark, #0256c2); - transform: translateY(-1px); -} - -.refresh-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; -} - -.refresh-btn:disabled:hover { - background: var(--who-blue, #0366d6); -} - -/* Authentication section */ -.auth-section { - background: var(--who-tertiary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 1.5rem; - margin: 1.5rem 0; - text-align: center; -} - -.login-section h3 { - color: var(--who-text-primary, #333); - margin-bottom: 1rem; -} - -.login-section p { - color: var(--who-text-secondary, #586069); - margin-bottom: 1rem; -} - -.authenticated-section { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; -} - -.authenticated-section p { - color: #28a745; - font-weight: 500; - margin: 0; -} - -.logout-btn { - background: #dc3545; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.logout-btn:hover { - background: #c82333; -} - -/* PR Filter section */ -.pr-filter-section { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.pr-filter-section label { - font-weight: 500; - color: var(--who-text-primary, #333); -} - -.filter-select { - padding: 0.5rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 6px; - background: var(--who-card-bg, #ffffff); - color: var(--who-text-primary, #333); - cursor: pointer; -} - -.filter-select:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -/* Comments section */ -.pr-comments-section { - margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--who-border-color, #e1e5e9); -} - -.pr-comments-section h4 { - margin: 0 0 1rem 0; - color: var(--who-text-primary, #333); - font-size: 1rem; -} - -.comments-loading { - color: var(--who-text-secondary, #586069); - font-style: italic; - padding: 0.5rem; -} - -.comments-list { - margin-bottom: 1rem; -} - -.comment-item { - background: var(--who-secondary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 0.75rem; - margin-bottom: 0.5rem; -} - -.comment-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.comment-avatar { - width: 20px; - height: 20px; - border-radius: 50%; -} - -.comment-author { - font-weight: 600; - color: var(--who-blue, #0366d6); - font-size: 0.9rem; -} - -.comment-date { - color: var(--who-text-secondary, #586069); - font-size: 0.8rem; - margin-left: auto; -} - -.comment-body { - color: var(--who-text-primary, #333); - font-size: 0.9rem; - line-height: 1.4; - white-space: pre-wrap; - word-break: break-word; -} - -.no-comments { - color: var(--who-text-secondary, #586069); - font-style: italic; - padding: 0.5rem; - text-align: center; -} - -.comment-input-section { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.comment-input { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 6px; - font-family: inherit; - font-size: 0.9rem; - resize: vertical; - min-height: 60px; -} - -.comment-input:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.submit-comment-btn { - align-self: flex-start; - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.submit-comment-btn:hover:not(:disabled) { - background: #0256cc; -} - -.submit-comment-btn:disabled { - background: var(--who-text-muted, #6a737d); - cursor: not-allowed; -} - -.comment-error-message { - background: #fff5f5; - border: 1px solid #fed7d7; - border-radius: 6px; - padding: 0.75rem; - color: #c53030; - font-size: 0.9rem; - margin: 0.25rem 0; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.comment-auth-message { - background: var(--who-tertiary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; - text-align: center; -} - -.comment-auth-message p { - margin: 0; - color: var(--who-text-secondary, #586069); -} - -.comment-auth-message a { - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; -} - -.comment-auth-message a:hover { - text-decoration: underline; -} - -/* Main action buttons */ -.main-actions { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 2rem; - flex-wrap: wrap; -} - -.contribute-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border-radius: 8px; - font-weight: 500; - text-decoration: none; - border: none; - cursor: pointer; - transition: all 0.2s ease; - font-size: 1rem; -} - -.contribute-btn.primary { - background: #28a745; - color: white; -} - -.contribute-btn.primary:hover { - background: #218838; - color: white; - text-decoration: none; -} - -.contribute-btn.secondary { - background: #0366d6; - color: white; -} - -.contribute-btn.secondary:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.contribute-btn.tertiary { - background: #6f42c1; - color: white; -} - -.contribute-btn.tertiary:hover { - background: #5a32a3; - color: white; - text-decoration: none; -} - -/* Top section with two cards */ -.top-section { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -.mascot-card, .main-branch-card { - background: var(--who-card-bg, #ffffff); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 2rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; -} - -.mascot-card:hover, .main-branch-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); -} - -.mascot-content { - display: flex; - align-items: flex-start; - gap: 2rem; -} - -.large-mascot { - width: 150px; - height: 150px; - object-fit: contain; - flex-shrink: 0; -} - -.explainer-content h2, .main-branch-content h2 { - color: var(--who-blue, #0366d6); - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.explainer-content p { - color: var(--who-text-secondary, #586069); - line-height: 1.6; - margin-bottom: 1rem; -} - -.source-code-link { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; - padding: 0.5rem 1rem; - border: 1px solid var(--who-blue, #0366d6); - border-radius: 6px; - transition: all 0.2s ease; -} - -.source-code-link:hover { - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; -} - -.main-branch-content { - text-align: center; -} - -.main-branch-content p { - color: var(--who-text-secondary, #586069); - line-height: 1.6; - margin-bottom: 1.5rem; -} - -.main-branch-actions { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.main-branch-link { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; - padding: 1rem 1.5rem; - border-radius: 8px; - font-weight: 500; - font-size: 1.1rem; - transition: background-color 0.2s ease; -} - -.main-branch-link:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.docs-link { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - color: var(--who-blue, #0366d6); - text-decoration: none; - padding: 0.75rem 1rem; - border: 1px solid var(--who-blue, #0366d6); - border-radius: 6px; - font-weight: 500; - transition: all 0.2s ease; -} - -.docs-link:hover { - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; -} - -/* PR section header */ -.pr-section-header { - text-align: center; - margin: 3rem 0 2rem 0; - padding: 2rem; - background: var(--who-secondary-bg, #f8f9fa); - border-radius: 12px; - border: 1px solid var(--who-border-color, #e1e5e9); -} - -.pr-header-content { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; -} - -.pr-header-text { - flex: 1; - text-align: left; -} - -.pr-header-actions { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.5rem; -} - -.refresh-btn { - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-size: 0.9rem; - font-weight: 500; - transition: background-color 0.2s ease; - min-width: 120px; -} - -.refresh-btn:hover:not(:disabled) { - background: var(--who-blue-dark, #0256cc); -} - -.refresh-btn:disabled { - background: #6c757d; - cursor: not-allowed; -} - -.cache-info { - font-size: 0.8rem; - color: var(--who-text-secondary, #586069); -} - -@media (max-width: 768px) { - .pr-header-content { - flex-direction: column; - text-align: center; - } - - .pr-header-text { - text-align: center; - } - - .pr-header-actions { - align-items: center; - } -} - -.pr-section-header h2 { - color: var(--who-blue, #0366d6); - margin: 0 0 0.5rem 0; - font-size: 2rem; -} - -.pr-section-header p { - color: var(--who-text-secondary, #586069); - font-size: 1.1rem; - margin: 0; -} - -/* Responsive adjustments for top section */ -@media (max-width: 768px) { - .top-section { - grid-template-columns: 1fr; - gap: 1rem; - } - - .mascot-content { - flex-direction: column; - text-align: center; - gap: 1rem; - } - - .large-mascot { - width: 120px; - height: 120px; - align-self: center; - } - - .main-branch-actions { - gap: 0.75rem; - } - - .pr-section-header { - padding: 1.5rem; - } - - .pr-section-header h2 { - font-size: 1.5rem; - } -} - -/* Cards styling - unified for PRs */ -.pr-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 1.5rem; - margin-bottom: 3rem; -} - -/* Branch and PR section specific styles */ -.pr-section { - margin-bottom: 3rem; -} - -.pr-controls { - margin-bottom: 2rem; - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - flex-wrap: wrap; -} - -.pr-search { - width: 100%; - max-width: 500px; - padding: 0.75rem 1rem; - border: 1px solid #e1e5e9; - border-radius: 8px; - font-size: 1rem; - background: #ffffff; -} - -.pr-search:focus { - outline: none; - border-color: #0366d6; - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.sort-select { - padding: 0.75rem 1rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - font-size: 1rem; - background: var(--who-card-bg, #ffffff); - color: var(--who-text-primary, #333); - cursor: pointer; - min-width: 200px; -} - -.sort-select:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.status-checking { - color: #0366d6; - font-size: 0.9rem; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.preview-card { - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 1.5rem; - background: var(--who-card-bg, #ffffff); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - position: relative; -} - -.preview-card:hover { - transform: translateY(-4px); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - border-color: var(--who-blue, #0366d6); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1rem; - gap: 1rem; -} - -.card-badges { - display: flex; - flex-direction: column; - gap: 0.5rem; - align-items: flex-end; - flex-shrink: 0; -} - -.item-name { - margin: 0; - font-size: 1.4rem; - color: var(--who-blue, #0366d6); - font-weight: 600; - word-break: break-word; - flex: 1; -} - -.commit-badge, .state-badge, .status-badge { - background: var(--who-hover-bg, #f1f3f4); - color: var(--who-text-secondary, #586069); - padding: 0.25rem 0.5rem; - border-radius: 6px; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 0.875rem; - font-weight: 500; - flex-shrink: 0; - white-space: nowrap; -} - -.status-badge.active { - background: #dcfce7; - color: #166534; -} - -.status-badge.not-found { - background: #fef3c7; - color: #92400e; -} - -.status-badge.errored { - background: #fee2e2; - color: #991b1b; -} - -.state-badge.open { - background: #dcfce7; - color: #166534; -} - -.state-badge.closed { - background: #fee2e2; - color: #991b1b; -} - -.card-body { - margin-bottom: 1rem; -} - -.item-date, .pr-meta { - color: var(--who-text-secondary, #586069); - font-size: 0.9rem; - margin: 0 0 1rem 0; -} - -.pr-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.pr-top-actions { - display: flex; - gap: 0.5rem; - margin: 0.75rem 0; - padding: 0.5rem 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.view-files-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: #60a5fa; - text-decoration: none; - padding: 0.5rem 1rem; - border-radius: 6px; - font-weight: 500; - font-size: 0.875rem; - background: rgba(96, 165, 250, 0.1); - border: 1px solid rgba(96, 165, 250, 0.3); - transition: all 0.2s ease; -} - -.view-files-btn:hover { - background: rgba(96, 165, 250, 0.2); - border-color: rgba(96, 165, 250, 0.5); - color: #93c5fd; - text-decoration: none; - transform: translateY(-1px); -} - -.preview-link, .pr-link, .copy-btn, .actions-link { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: white; - text-decoration: none; - padding: 0.75rem 1.5rem; - border-radius: 8px; - font-weight: 500; - transition: background-color 0.2s ease; - font-size: 0.9rem; - border: none; - cursor: pointer; -} - -.preview-link { - background: #0366d6; -} - -.preview-link:hover { - background: #0256cc; - text-decoration: none; - color: white; -} - -.copy-btn { - background: #28a745; -} - -.copy-btn:hover { - background: #218838; - color: white; -} - -.pr-link { - background: #6f42c1; -} - -.pr-link:hover { - background: #5a32a3; -} - -.actions-link { - background: #dc3545; - font-size: 0.8rem; - padding: 0.5rem 1rem; -} - -.actions-link:hover { - background: #c82333; - color: white; - text-decoration: none; -} - -.deployment-message { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.75rem; - border-radius: 8px; - font-size: 0.9rem; -} - -.building-message { - color: #92400e; - background: #fef3c7; - padding: 0.5rem; - border-radius: 6px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.error-message { - color: #991b1b; - background: #fee2e2; - padding: 0.5rem; - border-radius: 6px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.card-footer { - border-top: 1px solid #e1e5e9; - padding-top: 1rem; - margin-top: 1rem; -} - -.preview-path { - color: #6a737d; - font-size: 0.8rem; - word-break: break-all; -} - -.preview-url-link { - color: #0366d6; - text-decoration: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; -} - -.preview-url-link:hover { - text-decoration: underline; -} - -/* PR section specific styles moved to branch-section, pr-section above */ - -.pr-card { - border-left: 4px solid #0366d6; -} - -.pr-card .item-name { - font-size: 1.2rem; - line-height: 1.4; -} - -/* Pagination */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin-top: 2rem; -} - -.pagination-btn { - background: var(--who-hover-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - color: var(--who-blue, #0366d6); - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.2s ease; -} - -.pagination-btn:hover:not(:disabled) { - background: var(--who-selected-bg, #e1e7fd); - border-color: var(--who-blue, #0366d6); -} - -.pagination-btn:disabled { - color: var(--who-text-muted, #6a737d); - cursor: not-allowed; - opacity: 0.6; -} - -.pagination-info { - color: var(--who-text-secondary, #586069); - font-size: 0.9rem; -} - -.no-items { - grid-column: 1 / -1; - text-align: center; - padding: 3rem; - color: #6a737d; -} - -.no-items p { - margin: 0.5rem 0; -} - -.loading, .error { - text-align: center; - padding: 3rem; - color: #6a737d; -} - -.error { - color: #d73a49; -} - -/* Footer */ -.branch-listing-footer { - border-top: 1px solid var(--who-border-color, #e1e5e9); - padding: 2rem 0; - margin-top: 3rem; -} - -.footer-content { - display: flex; - justify-content: space-between; - align-items: center; - gap: 2rem; -} - -.footer-left { - flex-shrink: 0; -} - -.footer-center { - text-align: center; - color: var(--who-text-secondary, #6a737d); -} - -.footer-center p { - margin: 0.5rem 0; -} - -.source-link, .footer-center a { - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.source-link:hover, .footer-center a:hover { - text-decoration: underline; -} - -/* Discussion Summary Status Bar */ -.discussion-summary-bar { - background: var(--who-secondary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 0.75rem 1rem; - margin-top: 1rem; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - justify-content: space-between; - align-items: center; -} - -.discussion-summary-bar:hover { - background: var(--who-hover-bg, #f1f3f4); - border-color: var(--who-blue, #0366d6); -} - -.discussion-summary-text { - color: var(--who-text-primary, #333); - font-size: 0.9rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.discussion-summary-icon { - color: var(--who-blue, #0366d6); - font-weight: bold; -} - -.discussion-expand-icon { - color: var(--who-text-secondary, #586069); - transition: transform 0.2s ease; -} - -.discussion-expand-icon.expanded { - transform: rotate(90deg); -} - -/* Expanded Discussion Section */ -.discussion-expanded-section { - margin-top: 0.5rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - background: var(--who-card-bg, #ffffff); - overflow: hidden; -} - -.discussion-header { - background: var(--who-secondary-bg, #f8f9fa); - padding: 1rem; - border-bottom: 1px solid var(--who-border-color, #e1e5e9); - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0.5rem; -} - -.discussion-title { - font-weight: 600; - color: var(--who-text-primary, #333); - margin: 0; -} - -.discussion-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.discussion-action-btn { - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.375rem 0.75rem; - border-radius: 4px; - font-size: 0.8rem; - text-decoration: none; - cursor: pointer; - transition: background-color 0.2s ease; - display: inline-flex; - align-items: center; - gap: 0.25rem; -} - -.discussion-action-btn:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.discussion-action-btn.secondary { - background: #6f42c1; -} - -.discussion-action-btn.secondary:hover { - background: #5a32a3; -} - -.comment-input-section { - padding: 1rem; - border-bottom: 1px solid var(--who-border-color, #e1e5e9); - background: var(--who-tertiary-bg, #f8f9fa); -} - -.discussion-scroll-area { - max-height: 400px; - overflow-y: auto; - padding: 1rem; -} - -/* Contribute modal styles */ -.contribute-slide { - text-align: center; - padding: 1rem; -} - -.mascot-container { - margin-bottom: 1.5rem; - position: relative; - display: flex; - justify-content: center; - align-items: center; -} - -.contribute-mascot { - width: 120px; - height: 120px; - object-fit: contain; - margin: 0 0.5rem; -} - -.contribute-mascot.bug-report { - filter: sepia(1) hue-rotate(320deg) saturate(2); -} - -.contribute-mascot.coding-agent { - filter: sepia(1) hue-rotate(180deg) saturate(1.5); -} - -.contribute-mascot.community { - width: 80px; - height: 80px; -} - -.contribute-mascot.celebrate { - animation: bounce 2s infinite; -} - -@keyframes bounce { - 0%, 20%, 50%, 80%, 100% { - transform: translateY(0); - } - 40% { - transform: translateY(-10px); - } - 60% { - transform: translateY(-5px); - } -} - -.mascot-group { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 1rem; -} - -.thought-bubble { - position: absolute; - top: -20px; - right: 20%; - background: white; - border: 2px solid #0366d6; - border-radius: 50%; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - animation: float 3s ease-in-out infinite; -} - -@keyframes float { - 0%, 100% { - transform: translateY(0px); - } - 50% { - transform: translateY(-10px); - } -} - -.action-buttons { - display: flex; - flex-direction: column; - gap: 1rem; - margin: 2rem 0; - align-items: center; -} - -.contribute-footer { - background: #f8f9fa; - padding: 1.5rem; - border-radius: 8px; - border-left: 4px solid #28a745; - margin-top: 2rem; -} - -/* Responsive design */ -@media (max-width: 768px) { - .branch-listing { - padding: 1rem; - } - - .pr-cards { - grid-template-columns: 1fr; - gap: 1rem; - } - - .preview-card { - padding: 1rem; - } - - .card-header { - flex-direction: column; - gap: 0.5rem; - align-items: flex-start; - } - - .card-badges { - flex-direction: row; - align-items: flex-start; - gap: 0.5rem; - } - - .main-actions { - flex-direction: column; - align-items: center; - } - - .contribute-btn { - width: 100%; - max-width: 300px; - justify-content: center; - } - - .pr-actions { - justify-content: center; - } - - .pr-controls { - flex-direction: column; - align-items: stretch; - } - - .pr-filter-section { - justify-content: center; - } - - .authenticated-section { - flex-direction: column; - gap: 0.5rem; - } - - .copy-btn, .actions-link { - width: 100%; - justify-content: center; - } - - .pagination { - flex-direction: column; - gap: 0.5rem; - } - - .mascot-group { - flex-direction: column; - align-items: center; - } - - .action-buttons { - width: 100%; - } - - .contribute-btn { - width: 100%; - max-width: none; - } -} \ No newline at end of file diff --git a/src/components/BranchListing.js b/src/components/BranchListing.js deleted file mode 100644 index 57e31bd09..000000000 --- a/src/components/BranchListing.js +++ /dev/null @@ -1,1142 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { PageLayout } from './framework'; -import HelpModal from './HelpModal'; -import PATLogin from './PATLogin'; -import WorkflowStatus from './WorkflowStatus'; -import githubActionsService from '../services/githubActionsService'; -import branchListingCacheService from '../services/branchListingCacheService'; -import useThemeImage from '../hooks/useThemeImage'; -import './BranchListing.css'; - -const BranchListing = () => { - const [pullRequests, setPullRequests] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [prPage, setPrPage] = useState(1); - const [prSearchTerm, setPrSearchTerm] = useState(''); - - const [prSortBy, setPrSortBy] = useState('updated'); // updated, number, alphabetical - const [showContributeModal, setShowContributeModal] = useState(false); - const [deploymentStatuses, setDeploymentStatuses] = useState({}); - const [prFilter, setPrFilter] = useState('open'); // 'open', 'closed', 'all' - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [githubToken, setGithubToken] = useState(null); - const [prComments, setPrComments] = useState({}); - const [commentInputs, setCommentInputs] = useState({}); - const [submittingComments, setSubmittingComments] = useState({}); - const [expandedDiscussions, setExpandedDiscussions] = useState({}); - const [discussionSummaries, setDiscussionSummaries] = useState({}); - const [loadingSummaries, setLoadingSummaries] = useState(false); - const [workflowStatuses, setWorkflowStatuses] = useState({}); - const [loadingWorkflowStatuses, setLoadingWorkflowStatuses] = useState(false); - const [isRefreshing, setIsRefreshing] = useState(false); - const [cacheInfo, setCacheInfo] = useState(null); - - // Theme-aware mascot image - const mascotImage = useThemeImage('sgex-mascot.png'); - - const ITEMS_PER_PAGE = 10; - const GITHUB_OWNER = 'litlfred'; - const GITHUB_REPO = 'sgex'; - - // Function to manually refresh cache and reload data - const handleManualRefresh = useCallback(async () => { - setIsRefreshing(true); - - // Clear the cache to force fresh data - branchListingCacheService.forceRefresh(GITHUB_OWNER, GITHUB_REPO); - - // The fetchData function will be called by the useEffect when isRefreshing changes - }, []); - - // GitHub authentication functions - const handleAuthSuccess = (token, octokitInstance) => { - setGithubToken(token); - setIsAuthenticated(true); - // Store token for session - sessionStorage.setItem('github_token', token); - // Set token for GitHub Actions service - githubActionsService.setToken(token); - }; - - const handleLogout = () => { - setGithubToken(null); - setIsAuthenticated(false); - sessionStorage.removeItem('github_token'); - // Clear token from GitHub Actions service - githubActionsService.setToken(null); - }; - - // Function to fetch PR comments summary - const fetchPRCommentsSummary = useCallback(async (prNumber) => { - // Allow fetching comments even without authentication for read-only access - - try { - const headers = { - 'Accept': 'application/vnd.github.v3+json' - }; - - // Add authorization header only if token is available - if (githubToken) { - headers['Authorization'] = `token ${githubToken}`; - } - - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { headers } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch comments: ${response.status}`); - } - - const comments = await response.json(); - if (comments.length === 0) { - return { count: 0, lastComment: null }; - } - - const lastComment = comments[comments.length - 1]; - return { - count: comments.length, - lastComment: { - author: lastComment.user.login, - created_at: new Date(lastComment.created_at), - avatar_url: lastComment.user.avatar_url - } - }; - } catch (error) { - console.error(`Error fetching comment summary for PR ${prNumber}:`, error); - return null; - } - }, [githubToken]); - - // Function to fetch all PR comments (for expanded view) - const fetchAllPRComments = useCallback(async (prNumber) => { - // Allow fetching comments even without authentication for read-only access - - try { - const headers = { - 'Accept': 'application/vnd.github.v3+json' - }; - - // Add authorization header only if token is available - if (githubToken) { - headers['Authorization'] = `token ${githubToken}`; - } - - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { headers } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch comments: ${response.status}`); - } - - const comments = await response.json(); - return comments.map(comment => ({ - id: comment.id, - author: comment.user.login, - body: comment.body, - created_at: new Date(comment.created_at).toLocaleDateString(), - avatar_url: comment.user.avatar_url - })); - } catch (error) { - console.error(`Error fetching all comments for PR ${prNumber}:`, error); - return []; - } - }, [githubToken]); - - // Function to load discussion summaries for visible PRs - const loadDiscussionSummaries = useCallback(async (prs) => { - if (prs.length === 0) return; - - setLoadingSummaries(true); - const summaries = {}; - - for (const pr of prs) { - summaries[pr.number] = await fetchPRCommentsSummary(pr.number); - } - - setDiscussionSummaries(summaries); - setLoadingSummaries(false); - }, [fetchPRCommentsSummary]); - - // Function to toggle discussion expansion - const toggleDiscussion = async (prNumber) => { - const isExpanded = expandedDiscussions[prNumber]; - - if (!isExpanded) { - // Load all comments when expanding - const comments = await fetchAllPRComments(prNumber); - setPrComments(prev => ({ ...prev, [prNumber]: comments })); - } - - setExpandedDiscussions(prev => ({ - ...prev, - [prNumber]: !isExpanded - })); - }; - - // Function to get discussion summary text - const getDiscussionSummaryText = (prNumber) => { - const summary = discussionSummaries[prNumber]; - - if (loadingSummaries) { - return "Loading discussion..."; - } - - if (!summary || summary.count === 0) { - return "No comments yet"; - } - - const { count, lastComment } = summary; - const timeAgo = lastComment ? getTimeAgo(lastComment.created_at) : ''; - - return `${count} comment${count > 1 ? 's' : ''}, last by ${lastComment.author} ${timeAgo}`; - }; - - // Helper function to get relative time - const getTimeAgo = (date) => { - const now = new Date(); - const diffMs = now - date; - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return 'today'; - if (diffDays === 1) return '1 day ago'; - if (diffDays < 7) return `${diffDays} days ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`; - return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`; - }; - - // Function to submit a comment - const submitComment = async (prNumber, commentText) => { - if (!githubToken || !commentText.trim()) return false; - - setSubmittingComments(prev => ({ ...prev, [prNumber]: true })); - - try { - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { - method: 'POST', - headers: { - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - body: commentText - }) - } - ); - - if (!response.ok) { - throw new Error(`Failed to submit comment: ${response.status}`); - } - - // Clear the comment input - setCommentInputs(prev => ({ ...prev, [prNumber]: '' })); - - // Refresh both full comments (if expanded) and summary - if (expandedDiscussions[prNumber]) { - const updatedComments = await fetchAllPRComments(prNumber); - setPrComments(prev => ({ ...prev, [prNumber]: updatedComments })); - } - - // Refresh the discussion summary - const updatedSummary = await fetchPRCommentsSummary(prNumber); - setDiscussionSummaries(prev => ({ ...prev, [prNumber]: updatedSummary })); - - return true; - } catch (error) { - console.error(`Error submitting comment for PR ${prNumber}:`, error); - return false; - } finally { - setSubmittingComments(prev => ({ ...prev, [prNumber]: false })); - } - }; - - // Function to load workflow statuses for PR branches - const loadWorkflowStatuses = useCallback(async (prData) => { - if (!githubToken) return; - - setLoadingWorkflowStatuses(true); - - try { - // Get all PR branch names - const branchNames = prData.map(pr => pr.branchName); - - const uniqueBranchNames = [...new Set(branchNames)]; - const statuses = await githubActionsService.getWorkflowStatusForBranches(uniqueBranchNames); - - setWorkflowStatuses(statuses); - } catch (error) { - console.error('Error loading workflow statuses:', error); - } finally { - setLoadingWorkflowStatuses(false); - } - }, [githubToken]); - - // Function to trigger workflow for a branch - const triggerWorkflow = useCallback(async (branchName) => { - if (!githubToken) { - alert('Please authenticate to trigger workflows'); - return; - } - - try { - const success = await githubActionsService.triggerWorkflow(branchName); - if (success) { - alert(`Workflow triggered for branch: ${branchName}`); - // Refresh workflow statuses after a short delay - setTimeout(() => { - const currentPRs = pullRequests.length > 0 ? pullRequests : []; - loadWorkflowStatuses(currentPRs); - }, 2000); - } else { - alert(`Failed to trigger workflow for branch: ${branchName}`); - } - } catch (error) { - console.error('Error triggering workflow:', error); - alert(`Error triggering workflow: ${error.message}`); - } - }, [githubToken, pullRequests, loadWorkflowStatuses]); - - // Check for existing authentication on component mount - useEffect(() => { - const token = sessionStorage.getItem('github_token'); - if (token) { - setGithubToken(token); - setIsAuthenticated(true); - // Set token for GitHub Actions service - githubActionsService.setToken(token); - } - }, []); - - // Function to check deployment status - const checkDeploymentStatus = async (url) => { - try { - const response = await fetch(url, { method: 'HEAD' }); - if (response.ok) { - return 'active'; - } else if (response.status === 404) { - return 'not-found'; - } else { - return 'errored'; - } - } catch (error) { - return 'errored'; - } - }; - - // Function to check deployment statuses for PRs only - const checkAllDeploymentStatuses = useCallback(async (prData) => { - const statuses = {}; - - // Check PRs only - for (const pr of prData) { - const status = await checkDeploymentStatus(pr.url); - statuses[`pr-${pr.id}`] = status; - } - - setDeploymentStatuses(statuses); - }, []); - - // Function to copy URL to clipboard - const copyToClipboard = async (url, type, name) => { - try { - await navigator.clipboard.writeText(url); - // You could add a toast notification here - console.log(`Copied ${type} URL for ${name} to clipboard`); - } catch (error) { - // Fallback for browsers that don't support clipboard API - const textArea = document.createElement('textarea'); - textArea.value = url; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - console.log(`Copied ${type} URL for ${name} to clipboard (fallback)`); - } - }; - - // Sorting function for PRs - const sortPRs = (prs, sortBy) => { - return [...prs].sort((a, b) => { - switch (sortBy) { - case 'number': - return b.number - a.number; // Highest number first - case 'alphabetical': - return a.title.localeCompare(b.title); - case 'updated': - default: - const dateA = new Date(a.updatedAt); - const dateB = new Date(b.updatedAt); - return dateB - dateA; // Most recent first - } - }); - }; - - // "How to contribute" slideshow content - const contributeHelpTopic = { - id: 'how-to-contribute', - title: 'How to Contribute to SGEX', - type: 'slideshow', - content: [ - { - title: 'Welcome to SGEX - A Collaborative Workbench', - content: ` -
-
- SGEX Mascot -
-

What is SGEX?

-

SGEX is an experimental collaborative project developing a workbench of tools to make it easier and faster to develop high fidelity SMART Guidelines Digital Adaptation Kits.

-

Our mission is to empower healthcare organizations worldwide to create and maintain standards-compliant digital health implementations.

-
- ` - }, - { - title: 'Step 1: Report a Bug or Make a Feature Request', - content: ` -
-
- SGEX Mascot examining a bug -
-

🐛 Found something that needs fixing?

-

Every great contribution starts with identifying what can be improved:

-
    -
  • Bug reports: Help us identify and fix issues
  • -
  • Feature requests: Share ideas for new functionality
  • -
  • Documentation improvements: Make our guides clearer
  • -
  • User experience feedback: Tell us what's confusing
  • -
-

Click the mascot's help button on any page to quickly report issues!

-
- ` - }, - { - title: 'Step 2: Assignment to a Coding Agent', - content: ` -
-
- Robotic SGEX Mascot -
-

🤖 AI-Powered Development

-

Once your issue is triaged, it may be assigned to one of our coding agents:

-
    -
  • Automated analysis: AI agents analyze the requirements
  • -
  • Code generation: Initial implementations are created
  • -
  • Testing integration: Automated tests validate changes
  • -
  • Documentation updates: Keep documentation in sync
  • -
-

This hybrid approach combines human insight with AI efficiency.

-
- ` - }, - { - title: 'Step 3: Community Collaboration', - content: ` -
-
-
- SGEX Mascot 1 - SGEX Mascot 2 - SGEX Mascot 3 -
-
đŸ’Ģ
-
-

🌟 Real-time Evolution

-

The community drives continuous improvement through collaborative discussion:

-
    -
  • Code reviews: Community members review and suggest improvements
  • -
  • Testing feedback: Real-world testing by healthcare professionals
  • -
  • Knowledge sharing: Best practices emerge from collective experience
  • -
  • Iterative refinement: The workbench evolves based on actual usage
  • -
-

Together, we're building the future of digital health tooling!

-
- ` - }, - { - title: 'Get Started Today!', - content: ` -
-
- SGEX Mascot celebrating -
-

🚀 Ready to Contribute?

- - -
- ` - } - ] - }; - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - // First, try to get cached data - const cachedData = branchListingCacheService.getCachedData(GITHUB_OWNER, GITHUB_REPO); - - if (cachedData && !isRefreshing) { - console.log('Using cached data for PR listing'); - - // Filter PRs based on current filter state - const filteredCachedPRs = cachedData.pullRequests.filter(pr => { - if (prFilter === 'all') return true; - return pr.state === prFilter; - }); - - setPullRequests(filteredCachedPRs); - setCacheInfo(branchListingCacheService.getCacheInfo(GITHUB_OWNER, GITHUB_REPO)); - - // Still need to check deployment statuses as these change frequently - await checkAllDeploymentStatuses(filteredCachedPRs); - - // Load workflow statuses if authenticated - if (githubToken) { - await loadWorkflowStatuses(filteredCachedPRs); - } - - // Load discussion summaries for first page - await loadDiscussionSummaries(filteredCachedPRs.slice(0, ITEMS_PER_PAGE)); - return; - } - - // If no cached data or refreshing, fetch fresh data - console.log('Fetching fresh data from GitHub API'); - - // Fetch pull requests based on filter - const prState = prFilter === 'all' ? 'all' : prFilter; - console.log(`Fetching PRs with state: ${prState} from GitHub API`); - - const prResponse = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/pulls?state=${prState}&sort=updated&per_page=100`); - if (!prResponse.ok) { - const errorText = await prResponse.text(); - console.error(`GitHub API error: ${prResponse.status} - ${errorText}`); - throw new Error(`Failed to fetch pull requests: ${prResponse.status} - ${prResponse.statusText}`); - } - const prData = await prResponse.json(); - console.log(`Fetched ${prData.length} PRs from GitHub API`); - - // Format PR data - const formattedPRs = prData.map(pr => { - const safeBranchName = pr.head.ref.replace(/\//g, '-'); - return { - id: pr.id, - number: pr.number, - title: pr.title, - state: pr.state, - author: pr.user.login, - branchName: pr.head.ref, - safeBranchName: safeBranchName, - url: `./${safeBranchName}/`, - prUrl: pr.html_url, - updatedAt: new Date(pr.updated_at).toLocaleDateString(), - createdAt: new Date(pr.created_at).toLocaleDateString() - }; - }); - - // Cache the fresh data (we only cache PRs since branches were removed) - branchListingCacheService.setCachedData(GITHUB_OWNER, GITHUB_REPO, [], formattedPRs); - setCacheInfo(branchListingCacheService.getCacheInfo(GITHUB_OWNER, GITHUB_REPO)); - - setPullRequests(formattedPRs); - - // Check deployment statuses for PRs only - await checkAllDeploymentStatuses(formattedPRs); - - // Load workflow statuses if authenticated - if (githubToken) { - await loadWorkflowStatuses(formattedPRs); - } - - // Load discussion summaries for PRs - available for all users - await loadDiscussionSummaries(formattedPRs.slice(0, ITEMS_PER_PAGE)); - } catch (err) { - console.error('Error fetching data:', err); - setError(err.message); - - // Check if this is a network/CORS issue - if (err.message.includes('Failed to fetch') || err.message.includes('CORS')) { - console.log('Network/CORS error detected, checking if we can use cache or fallback data...'); - - // Try to use any existing cached data even if stale - const cachedDataRaw = localStorage.getItem(branchListingCacheService.getCacheKey(GITHUB_OWNER, GITHUB_REPO)); - if (cachedDataRaw) { - try { - const parsed = JSON.parse(cachedDataRaw); - console.log('Using stale cached data due to network error'); - setPullRequests(parsed.pullRequests.filter(pr => { - if (prFilter === 'all') return true; - return pr.state === prFilter; - })); - setCacheInfo({ - exists: true, - stale: true, - ageMinutes: Math.round((Date.now() - parsed.timestamp) / (60 * 1000)), - prCount: parsed.pullRequests?.length || 0 - }); - setError('Using cached data (network error occurred)'); - return; - } catch (parseError) { - console.error('Error parsing stale cache:', parseError); - } - } - } - - // Only use fallback data in development or when GitHub API is blocked - if (process.env.NODE_ENV === 'development' || err.message.includes('Failed to fetch')) { - console.log('Using fallback mock data for demonstration...'); - const mockPRs = [ - { - id: 1, - number: 123, - title: 'Improve multi-page selector landing page for GitHub deployment', - state: 'open', - author: 'copilot', - branchName: 'copilot/fix-459', - safeBranchName: 'copilot-fix-459', - url: './copilot-fix-459/', - prUrl: 'https://github.com/litlfred/sgex/pull/123', - updatedAt: new Date().toLocaleDateString(), - createdAt: new Date(Date.now() - 86400000).toLocaleDateString() - }, - { - id: 2, - number: 122, - title: 'Add dark mode support', - state: 'closed', - author: 'developer', - branchName: 'feature/dark-mode', - safeBranchName: 'feature-dark-mode', - url: './feature-dark-mode/', - prUrl: 'https://github.com/litlfred/sgex/pull/122', - updatedAt: new Date(Date.now() - 172800000).toLocaleDateString(), - createdAt: new Date(Date.now() - 345600000).toLocaleDateString() - }, - { - id: 3, - number: 121, - title: 'Fix authentication flow', - state: 'open', - author: 'contributor', - branchName: 'fix/auth-flow', - safeBranchName: 'fix-auth-flow', - url: './fix-auth-flow/', - prUrl: 'https://github.com/litlfred/sgex/pull/121', - updatedAt: new Date(Date.now() - 259200000).toLocaleDateString(), - createdAt: new Date(Date.now() - 432000000).toLocaleDateString() - } - ]; - - setPullRequests(mockPRs); - setError(null); // Clear error since we have fallback data - } - } finally { - setLoading(false); - setIsRefreshing(false); // Reset refresh state - } - }; - - fetchData(); - }, [checkAllDeploymentStatuses, prFilter, githubToken, loadWorkflowStatuses, loadDiscussionSummaries, isRefreshing]); - - // Load summaries for visible PRs when page changes - useEffect(() => { - if (pullRequests.length > 0) { - const filtered = pullRequests.filter(pr => - pr.title.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.author.toLowerCase().includes(prSearchTerm.toLowerCase()) - ); - const sorted = sortPRs(filtered, prSortBy); - const paginated = sorted.slice((prPage - 1) * ITEMS_PER_PAGE, prPage * ITEMS_PER_PAGE); - loadDiscussionSummaries(paginated); - } - }, [prPage, prSearchTerm, prSortBy, pullRequests, loadDiscussionSummaries]); - - // Filter and sort PRs based on search and sorting - const filteredPRs = pullRequests.filter(pr => - pr.title.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.author.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.branchName.toLowerCase().includes(prSearchTerm.toLowerCase()) - ); - const sortedPRs = sortPRs(filteredPRs, prSortBy); - const paginatedPRs = sortedPRs.slice((prPage - 1) * ITEMS_PER_PAGE, prPage * ITEMS_PER_PAGE); - const totalPRPages = Math.ceil(sortedPRs.length / ITEMS_PER_PAGE); - - if (loading) { - return ( - -
-

SGEX Icon SGEX

-

a collaborative workbench for WHO SMART Guidelines

-
Loading previews...
-
-
- ); - } - - if (error) { - return ( - -
-

SGEX Icon SGEX

-

a collaborative workbench for WHO SMART Guidelines

-
-

Failed to load previews: {error}

-

Please try refreshing the page or check back later.

-
-
-
- ); - } - - return ( - -
- {/* Top Section: Two cards side by side */} -
- {/* Mascot and Explainer Card */} -
-
- SGEX Mascot -
-

SGEX Deployment Selection

-

Welcome to the SGEX deployment selection page. Here you can browse and access all available pull request previews for the WHO SMART Guidelines Exchange collaborative workbench.

-

Each pull request is automatically deployed to its own preview environment for testing and collaboration.

- - đŸ“Ļ View Source Code - -
-
-
- - {/* Main Branch Access Card */} -
-
-

🚀 Main Branch

-

Access the stable main branch of the SGEX workbench with the latest published features.

- -
-
-
- - {/* Authentication Section */} -
- {!isAuthenticated ? ( -
-

🔐 GitHub Authentication

-

Login with your GitHub Personal Access Token to view and add comments to pull requests:

- -
- ) : ( -
-

✅ Authenticated - You can now view and add comments to pull requests

- -
- )} -
- - {/* Main Actions */} -
- - - 🐛 Report a Bug - -
- - {/* PR Section Header */} -
-
-
-

🔄 Pull Request Previews ({sortedPRs.length})

-

Browse and test pull request changes in isolated preview environments

-
-
- - {cacheInfo && ( -
- - {cacheInfo.exists - ? `📊 Data cached (${cacheInfo.ageMinutes}m old)` - : 'Fresh data' - } - -
- )} -
-
-
- - {/* PR Section */} -
-
-
- - -
- { - setPrSearchTerm(e.target.value); - setPrPage(1); // Reset to first page when searching - }} - className="pr-search" - /> - -
- -
- {paginatedPRs.length === 0 ? ( -
- {prSearchTerm ? ( -

No pull requests match your search "{prSearchTerm}".

- ) : ( -

No pull request previews available at the moment.

- )} -
- ) : ( - paginatedPRs.map((pr) => { - const statusKey = `pr-${pr.id}`; - const deploymentStatus = deploymentStatuses[statusKey]; - - return ( -
-
-

#{pr.number}: {pr.title}

-
- - {pr.state === 'open' ? 'đŸŸĸ' : '🔴'} {pr.state} - - {deploymentStatus && ( - - {deploymentStatus === 'active' && 'đŸŸĸ Active'} - {deploymentStatus === 'not-found' && '🟡 Building'} - {deploymentStatus === 'errored' && '🔴 Error'} - - )} -
-
- -
-

- Branch: {pr.branchName} â€ĸ Author: {pr.author} -

-

- Created: {pr.createdAt} â€ĸ Updated: {pr.updatedAt} -

- - {/* PR Actions - View Files button at the top */} - - - {/* Discussion Summary Section - Show for all users */} -
- {/* Discussion Summary Status Bar */} -
toggleDiscussion(pr.number)} - > -
- đŸ’Ŧ - {getDiscussionSummaryText(pr.number)} -
- - â–ļ - -
- - {/* Expanded Discussion Section */} - {expandedDiscussions[pr.number] && ( -
-
-

Discussion

- -
- - {/* Comment Input at Top - Only show for authenticated users */} - {isAuthenticated ? ( -
-