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: `
-
-
-
-
-
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: `
-
-
-
-
-
đ 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: `
-
-
-
-
-
đ¤ 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: `
-
-
-
đ 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: `
-
-
-
-
-
đ 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
-
a collaborative workbench for WHO SMART Guidelines
-
Loading previews...
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
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 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
-
- Logout
-
-
- )}
-
-
- {/* Main Actions */}
-
-
- {/* PR Section Header */}
-
-
-
-
đ Pull Request Previews ({sortedPRs.length})
-
Browse and test pull request changes in isolated preview environments
-
-
-
- {isRefreshing ? 'đ Refreshing...' : 'đ Refresh'}
-
- {cacheInfo && (
-
-
- {cacheInfo.exists
- ? `đ Data cached (${cacheInfo.ageMinutes}m old)`
- : 'Fresh data'
- }
-
-
- )}
-
-
-
-
- {/* PR Section */}
-
-
-
- Filter PRs:
- {
- setPrFilter(e.target.value);
- setPrPage(1); // Reset to first page when filtering
- }}
- className="filter-select"
- >
- Open PRs Only
- Closed PRs Only
- All PRs
-
-
-
{
- setPrSearchTerm(e.target.value);
- setPrPage(1); // Reset to first page when searching
- }}
- className="pr-search"
- />
-
{
- setPrSortBy(e.target.value);
- setPrPage(1); // Reset to first page when sorting
- }}
- className="sort-select"
- >
- Sort by Recent Updates
- Sort by PR Number
- Sort Alphabetically
-
-
-
-
- {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] && (
-
-
-
- {/* Comment Input at Top - Only show for authenticated users */}
- {isAuthenticated ? (
-
-
- ) : (
-
- )}
-
- {/* Scrollable Comments Area */}
-
- {!prComments[pr.number] ? (
-
Loading full discussion...
- ) : prComments[pr.number].length > 0 ? (
-
- {prComments[pr.number].map((comment) => (
-
-
-
-
{comment.author}
-
{comment.created_at}
-
-
{comment.body}
-
- ))}
-
- ) : (
-
- No comments yet. {isAuthenticated ? 'Be the first to comment!' : 'Sign in to be the first to comment!'}
-
- )}
-
-
- )}
-
-
-
- {deploymentStatus === 'active' ? (
-
- đ View Preview
-
- ) : deploymentStatus === 'not-found' ? (
-
-
- đ Deployment in progress. Please check back in a few minutes.
-
-
- ) : deploymentStatus === 'errored' ? (
-
-
- â Deployment failed. Please check the GitHub Actions logs or contact support.
-
-
- View Actions Log
-
-
- ) : (
-
- đ View Preview
-
- )}
-
-
copyToClipboard(pr.url, 'PR', `#${pr.number}`)}
- title="Copy URL to clipboard"
- >
- đ Copy URL
-
-
-
- đ View PR
-
-
-
- {/* Workflow Status */}
-
-
-
-
-
- );
- })
- )}
-
-
- {totalPRPages > 1 && (
-
- setPrPage(Math.max(1, prPage - 1))}
- disabled={prPage === 1}
- >
- â Previous
-
-
- Page {prPage} of {totalPRPages} ({sortedPRs.length} total)
-
- setPrPage(Math.min(totalPRPages, prPage + 1))}
- disabled={prPage === totalPRPages}
- >
- Next â
-
-
- )}
-
-
- {showContributeModal && (
-
setShowContributeModal(false)}
- />
- )}
-
-
- );
-};
-
-export default BranchListing;
\ No newline at end of file
diff --git a/src/services/branchListingCacheService.js b/src/services/branchListingCacheService.js
deleted file mode 100644
index d649ebdc1..000000000
--- a/src/services/branchListingCacheService.js
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Branch Listing Cache Service
- * Manages caching of branch and PR preview data with 5-minute expiry
- */
-
-import logger from '../utils/logger.js';
-
-class BranchListingCacheService {
- constructor() {
- this.CACHE_KEY_PREFIX = 'sgex_branch_listing_cache_';
- this.CACHE_EXPIRY_MINUTES = 5; // Cache expires after 5 minutes as per requirements
- this.logger = logger.getLogger('BranchListingCacheService');
- this.logger.debug('BranchListingCacheService initialized', {
- cacheExpiryMinutes: this.CACHE_EXPIRY_MINUTES
- });
- }
-
- /**
- * Generate cache key for branch listing data
- */
- getCacheKey(owner, repo) {
- return `${this.CACHE_KEY_PREFIX}${owner}_${repo}`;
- }
-
- /**
- * Check if cached data is stale (older than 5 minutes)
- */
- isStale(timestamp) {
- const now = Date.now();
- const cacheAge = now - timestamp;
- const maxAge = this.CACHE_EXPIRY_MINUTES * 60 * 1000; // 5 minutes in milliseconds
- return cacheAge > maxAge;
- }
-
- /**
- * Get cached branch listing data (branches and PRs)
- * Returns null if cache doesn't exist or is stale
- */
- getCachedData(owner, repo) {
- try {
- const cacheKey = this.getCacheKey(owner, repo);
- this.logger.cache('get', cacheKey);
-
- const cachedData = localStorage.getItem(cacheKey);
-
- if (!cachedData) {
- this.logger.cache('miss', cacheKey, 'No cached data found');
- return null;
- }
-
- const parsed = JSON.parse(cachedData);
-
- // Check if cache is stale
- if (this.isStale(parsed.timestamp)) {
- // Remove stale cache
- this.logger.cache('expired', cacheKey, { age: Date.now() - parsed.timestamp });
- localStorage.removeItem(cacheKey);
- return null;
- }
-
- this.logger.cache('hit', cacheKey, {
- branchCount: parsed.branches?.length || 0,
- prCount: parsed.pullRequests?.length || 0,
- age: Date.now() - parsed.timestamp
- });
-
- return {
- branches: parsed.branches,
- pullRequests: parsed.pullRequests,
- timestamp: parsed.timestamp,
- owner: parsed.owner,
- repo: parsed.repo
- };
- } catch (error) {
- const cacheKey = this.getCacheKey(owner, repo);
- this.logger.error('Error reading branch listing cache', { cacheKey, error: error.message });
- console.warn('Error reading branch listing cache:', error);
- return null;
- }
- }
-
- /**
- * Cache branch listing data (branches and PRs)
- */
- setCachedData(owner, repo, branches, pullRequests) {
- try {
- const cacheKey = this.getCacheKey(owner, repo);
- const cacheData = {
- branches,
- pullRequests,
- timestamp: Date.now(),
- owner,
- repo
- };
-
- this.logger.cache('set', cacheKey, {
- branchCount: branches?.length || 0,
- prCount: pullRequests?.length || 0,
- owner,
- repo
- });
-
- localStorage.setItem(cacheKey, JSON.stringify(cacheData));
- return true;
- } catch (error) {
- const cacheKey = this.getCacheKey(owner, repo);
- this.logger.error('Error caching branch listing data', { cacheKey, error: error.message });
- console.warn('Error caching branch listing data:', error);
- return false;
- }
- }
-
- /**
- * Clear cache for a specific repository
- */
- clearCache(owner, repo) {
- try {
- const cacheKey = this.getCacheKey(owner, repo);
- this.logger.cache('clear', cacheKey, { owner, repo });
- localStorage.removeItem(cacheKey);
- return true;
- } catch (error) {
- const cacheKey = this.getCacheKey(owner, repo);
- this.logger.error('Error clearing branch listing cache', { cacheKey, error: error.message });
- console.warn('Error clearing branch listing cache:', error);
- return false;
- }
- }
-
- /**
- * Clear all branch listing caches
- */
- clearAllCaches() {
- try {
- const keys = Object.keys(localStorage);
- let clearedCount = 0;
- keys.forEach(key => {
- if (key.startsWith(this.CACHE_KEY_PREFIX)) {
- localStorage.removeItem(key);
- clearedCount++;
- }
- });
- this.logger.debug('Cleared all branch listing caches', { clearedCount });
- return true;
- } catch (error) {
- this.logger.error('Error clearing all branch listing caches', { error: error.message });
- console.warn('Error clearing all branch listing caches:', error);
- return false;
- }
- }
-
- /**
- * Get cache info for debugging
- */
- getCacheInfo(owner, repo) {
- const cached = this.getCachedData(owner, repo);
- if (!cached) {
- return { exists: false, stale: true };
- }
-
- const age = Date.now() - cached.timestamp;
- const ageMinutes = Math.round(age / (60 * 1000));
-
- return {
- exists: true,
- stale: this.isStale(cached.timestamp),
- age: age,
- ageMinutes: ageMinutes,
- branchCount: cached.branches?.length || 0,
- prCount: cached.pullRequests?.length || 0,
- timestamp: new Date(cached.timestamp).toISOString()
- };
- }
-
- /**
- * Force refresh cache - clear existing cache to force fresh data fetch
- */
- forceRefresh(owner, repo) {
- this.logger.info('Force refresh requested', { owner, repo });
- return this.clearCache(owner, repo);
- }
-}
-
-// Create singleton instance
-const branchListingCacheService = new BranchListingCacheService();
-
-export default branchListingCacheService;
\ No newline at end of file
diff --git a/src/services/branchListingCacheService.test.js b/src/services/branchListingCacheService.test.js
deleted file mode 100644
index 95e9a36dd..000000000
--- a/src/services/branchListingCacheService.test.js
+++ /dev/null
@@ -1,230 +0,0 @@
-/**
- * Branch Listing Cache Service Tests
- */
-
-// Create a working localStorage mock FIRST
-let mockStore = {};
-
-const localStorageMock = {
- getItem: jest.fn((key) => {
- return mockStore[key] || null;
- }),
- setItem: jest.fn((key, value) => {
- if (mockStore._simulateQuotaExceeded) {
- delete mockStore._simulateQuotaExceeded;
- throw new Error('Storage quota exceeded');
- }
- mockStore[key] = value.toString();
- }),
- removeItem: jest.fn((key) => {
- delete mockStore[key];
- }),
- clear: jest.fn(() => {
- mockStore = {};
- }),
- get length() {
- return Object.keys(mockStore).length;
- },
- key: jest.fn((index) => Object.keys(mockStore)[index] || null),
- // Test helpers
- _reset() {
- mockStore = {};
- this.getItem.mockClear();
- this.setItem.mockClear();
- this.removeItem.mockClear();
- this.clear.mockClear();
- }
-};
-
-// Replace global localStorage BEFORE any imports
-Object.defineProperty(global, 'localStorage', {
- value: localStorageMock,
- writable: true
-});
-
-// Also mock window.localStorage if it exists
-if (typeof window !== 'undefined') {
- Object.defineProperty(window, 'localStorage', {
- value: localStorageMock,
- writable: true
- });
-}
-
-// Mock logger to avoid localStorage conflicts
-jest.mock('../utils/logger', () => ({
- __esModule: true,
- default: {
- getLogger: jest.fn(() => ({
- debug: jest.fn(),
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- cache: jest.fn()
- }))
- }
-}));
-
-// NOW import the cache service after mocks are set up
-import branchListingCacheService from './branchListingCacheService';
-
-describe('BranchListingCacheService', () => {
- const testOwner = 'litlfred';
- const testRepo = 'sgex';
- const testBranches = [
- { name: 'main', commit: { sha: 'abc123' } },
- { name: 'feature-branch', commit: { sha: 'def456' } }
- ];
- const testPRs = [
- { id: 1, number: 123, title: 'Test PR', state: 'open' },
- { id: 2, number: 124, title: 'Another PR', state: 'closed' }
- ];
-
- beforeEach(() => {
- localStorageMock._reset();
- });
-
- describe('Cache Key Generation', () => {
- test('should generate correct cache key', () => {
- const key = branchListingCacheService.getCacheKey(testOwner, testRepo);
- expect(key).toBe('sgex_branch_listing_cache_litlfred_sgex');
- });
- });
-
- describe('Cache Expiry', () => {
- test('should detect stale cache (older than 5 minutes)', () => {
- const oldTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago
- const recentTimestamp = Date.now() - (3 * 60 * 1000); // 3 minutes ago
-
- expect(branchListingCacheService.isStale(oldTimestamp)).toBe(true);
- expect(branchListingCacheService.isStale(recentTimestamp)).toBe(false);
- });
- });
-
- describe('Cache Operations', () => {
- test('should cache and retrieve data successfully', () => {
- // Cache data
- const result = branchListingCacheService.setCachedData(
- testOwner,
- testRepo,
- testBranches,
- testPRs
- );
- expect(result).toBe(true);
- expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
-
- // Retrieve cached data
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).not.toBeNull();
- expect(cached.branches).toEqual(testBranches);
- expect(cached.pullRequests).toEqual(testPRs);
- expect(cached.owner).toBe(testOwner);
- expect(cached.repo).toBe(testRepo);
- expect(cached.timestamp).toBeCloseTo(Date.now(), -2);
- });
-
- test('should return null for non-existent cache', () => {
- const cached = branchListingCacheService.getCachedData('nonexistent', 'repo');
- expect(cached).toBeNull();
- });
-
- test('should return null and remove stale cache', () => {
- const cacheKey = branchListingCacheService.getCacheKey(testOwner, testRepo);
-
- // Set stale data directly in localStorage
- const staleData = {
- branches: testBranches,
- pullRequests: testPRs,
- timestamp: Date.now() - (6 * 60 * 1000), // 6 minutes ago
- owner: testOwner,
- repo: testRepo
- };
- mockStore[cacheKey] = JSON.stringify(staleData);
-
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).toBeNull();
- expect(localStorageMock.removeItem).toHaveBeenCalledWith(cacheKey);
- });
-
- test('should clear specific cache', () => {
- // Set cache first
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).not.toBeNull();
-
- // Clear cache
- const result = branchListingCacheService.clearCache(testOwner, testRepo);
- expect(result).toBe(true);
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).toBeNull();
- });
-
- test('should clear all caches', () => {
- // Set multiple caches
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
- branchListingCacheService.setCachedData('other', 'repo', [], []);
-
- const result = branchListingCacheService.clearAllCaches();
- expect(result).toBe(true);
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).toBeNull();
- expect(branchListingCacheService.getCachedData('other', 'repo')).toBeNull();
- });
- });
-
- describe('Cache Info', () => {
- test('should provide correct cache info for existing cache', () => {
- // Set cache
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
-
- const info = branchListingCacheService.getCacheInfo(testOwner, testRepo);
- expect(info.exists).toBe(true);
- expect(info.stale).toBe(false);
- expect(info.branchCount).toBe(2);
- expect(info.prCount).toBe(2);
- expect(info.ageMinutes).toBe(0);
- });
-
- test('should provide correct cache info for non-existent cache', () => {
- const info = branchListingCacheService.getCacheInfo('nonexistent', 'repo');
- expect(info.exists).toBe(false);
- expect(info.stale).toBe(true);
- });
- });
-
- describe('Force Refresh', () => {
- test('should clear cache when force refresh is called', () => {
- // Set cache
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).not.toBeNull();
-
- // Force refresh
- const result = branchListingCacheService.forceRefresh(testOwner, testRepo);
- expect(result).toBe(true);
-
- // Verify cache is cleared
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).toBeNull();
- });
- });
-
- describe('Error Handling', () => {
- test('should handle localStorage errors gracefully', () => {
- // Trigger storage quota exceeded error
- mockStore._simulateQuotaExceeded = true;
-
- const result = branchListingCacheService.setCachedData(
- testOwner,
- testRepo,
- testBranches,
- testPRs
- );
- expect(result).toBe(false);
- });
-
- test('should handle JSON parsing errors gracefully', () => {
- const cacheKey = branchListingCacheService.getCacheKey(testOwner, testRepo);
-
- // Set invalid JSON data
- mockStore[cacheKey] = 'invalid json{';
-
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).toBeNull();
- });
- });
-});
\ No newline at end of file
diff --git a/src/services/branchListingCacheService.test.js.bak b/src/services/branchListingCacheService.test.js.bak
deleted file mode 100644
index b14d794af..000000000
--- a/src/services/branchListingCacheService.test.js.bak
+++ /dev/null
@@ -1,254 +0,0 @@
-/**
- * Branch Listing Cache Service Tests
- */
-
-// Mock logger to avoid localStorage conflicts
-jest.mock('../utils/logger', () => ({
- __esModule: true,
- default: {
- getLogger: jest.fn(() => ({
- debug: jest.fn(),
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- cache: jest.fn()
- }))
- }
-}));
-
-import branchListingCacheService from './branchListingCacheService';
-
-// Mock localStorage
-let mockLocalStorage = {};
-
-const localStorageMock = (() => {
- let store = {};
-
- return {
- getItem(key) {
- console.log('getItem called with key:', key, 'found:', key in store);
- return store[key] || null;
- },
- setItem(key, value) {
- console.log('setItem called with key:', key, 'value length:', value.length);
- // Simulate storage quota exceeded for specific test
- if (store._simulateQuotaExceeded) {
- delete store._simulateQuotaExceeded;
- throw new Error('Storage quota exceeded');
- }
- store[key] = value;
- console.log('store after setItem:', Object.keys(store));
- },
- removeItem(key) {
- delete store[key];
- },
- clear() {
- store = {};
- },
- get length() {
- return Object.keys(store).length;
- },
- key(index) {
- return Object.keys(store)[index] || null;
- },
- // For testing
- _getStore() {
- return store;
- },
- _setStore(newStore) {
- store = newStore;
- }
- };
-})();
-
-// Create spies for the mock methods
-const setItemSpy = jest.spyOn(localStorageMock, 'setItem');
-const getItemSpy = jest.spyOn(localStorageMock, 'getItem');
-const removeItemSpy = jest.spyOn(localStorageMock, 'removeItem');
-
-Object.defineProperty(global, 'localStorage', {
- value: localStorageMock,
- writable: true
-});
-
-describe('BranchListingCacheService', () => {
- const testOwner = 'litlfred';
- const testRepo = 'sgex';
- const testBranches = [
- { name: 'main', commit: { sha: 'abc123' } },
- { name: 'feature-branch', commit: { sha: 'def456' } }
- ];
- const testPRs = [
- { id: 1, number: 123, title: 'Test PR', state: 'open' },
- { id: 2, number: 124, title: 'Another PR', state: 'closed' }
- ];
-
- beforeEach(() => {
- localStorageMock.clear();
- jest.clearAllMocks();
- });
-
- describe('Cache Key Generation', () => {
- test('should generate correct cache key', () => {
- const key = branchListingCacheService.getCacheKey(testOwner, testRepo);
- expect(key).toBe('sgex_branch_listing_cache_litlfred_sgex');
- });
- });
-
- describe('Cache Expiry', () => {
- test('should detect stale cache (older than 5 minutes)', () => {
- const now = Date.now();
- const fiveMinutesAgo = now - (5 * 60 * 1000);
- const sixMinutesAgo = now - (6 * 60 * 1000);
-
- expect(branchListingCacheService.isStale(fiveMinutesAgo)).toBe(false);
- expect(branchListingCacheService.isStale(sixMinutesAgo)).toBe(true);
- });
- });
-
- describe('Cache Operations', () => {
- test('should cache and retrieve data successfully', () => {
- // Cache data
- const result = branchListingCacheService.setCachedData(
- testOwner,
- testRepo,
- testBranches,
- testPRs
- );
- expect(result).toBe(true);
-
- // Debug: Check what's in localStorage and if setItem was called
- const cacheKey = branchListingCacheService.getCacheKey(testOwner, testRepo);
- console.log('Cache key:', cacheKey);
- console.log('setItem calls:', setItemSpy.mock.calls);
- console.log('localStorageStore:', localStorageMock._getStore());
-
- const rawData = localStorageMock.getItem(cacheKey);
- console.log('Raw localStorage data:', rawData);
-
- if (rawData) {
- const parsedData = JSON.parse(rawData);
- console.log('Parsed data:', parsedData);
- }
-
- // Retrieve cached data
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).not.toBeNull();
- expect(cached.branches).toEqual(testBranches);
- expect(cached.pullRequests).toEqual(testPRs);
- expect(cached.owner).toBe(testOwner);
- expect(cached.repo).toBe(testRepo);
- expect(cached.timestamp).toBeCloseTo(Date.now(), -2); // Within 100ms
- });
-
- test('should return null for non-existent cache', () => {
- const cached = branchListingCacheService.getCachedData('nonexistent', 'repo');
- expect(cached).toBeNull();
- });
-
- test('should return null and remove stale cache', () => {
- // Mock old timestamp (6 minutes ago)
- const oldTimestamp = Date.now() - (6 * 60 * 1000);
- const staleData = {
- branches: testBranches,
- pullRequests: testPRs,
- timestamp: oldTimestamp,
- owner: testOwner,
- repo: testRepo
- };
-
- const cacheKey = branchListingCacheService.getCacheKey(testOwner, testRepo);
- mockLocalStorage[cacheKey] = JSON.stringify(staleData);
-
- // Should return null and remove stale cache
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).toBeNull();
- expect(removeItemSpy).toHaveBeenCalledWith(cacheKey);
- });
-
- test('should clear specific cache', () => {
- // Set cache
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
-
- // Clear cache
- const result = branchListingCacheService.clearCache(testOwner, testRepo);
- expect(result).toBe(true);
-
- // Verify cache is gone
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).toBeNull();
- });
-
- test('should clear all caches', () => {
- // Set multiple caches
- branchListingCacheService.setCachedData('owner1', 'repo1', testBranches, testPRs);
- branchListingCacheService.setCachedData('owner2', 'repo2', testBranches, testPRs);
-
- // Clear all caches
- const result = branchListingCacheService.clearAllCaches();
- expect(result).toBe(true);
-
- // Verify all caches are gone
- expect(branchListingCacheService.getCachedData('owner1', 'repo1')).toBeNull();
- expect(branchListingCacheService.getCachedData('owner2', 'repo2')).toBeNull();
- });
- });
-
- describe('Cache Info', () => {
- test('should provide correct cache info for existing cache', () => {
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
-
- const info = branchListingCacheService.getCacheInfo(testOwner, testRepo);
- expect(info.exists).toBe(true);
- expect(info.stale).toBe(false);
- expect(info.branchCount).toBe(2);
- expect(info.prCount).toBe(2);
- expect(info.ageMinutes).toBe(0);
- });
-
- test('should provide correct cache info for non-existent cache', () => {
- const info = branchListingCacheService.getCacheInfo('nonexistent', 'repo');
- expect(info.exists).toBe(false);
- expect(info.stale).toBe(true);
- });
- });
-
- describe('Force Refresh', () => {
- test('should clear cache when force refresh is called', () => {
- // Set cache
- branchListingCacheService.setCachedData(testOwner, testRepo, testBranches, testPRs);
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).not.toBeNull();
-
- // Force refresh
- const result = branchListingCacheService.forceRefresh(testOwner, testRepo);
- expect(result).toBe(true);
-
- // Verify cache is cleared
- expect(branchListingCacheService.getCachedData(testOwner, testRepo)).toBeNull();
- });
- });
-
- describe('Error Handling', () => {
- test('should handle localStorage errors gracefully', () => {
- // Trigger storage quota exceeded error
- localStorageMock._getStore()._simulateQuotaExceeded = true;
-
- const result = branchListingCacheService.setCachedData(
- testOwner,
- testRepo,
- testBranches,
- testPRs
- );
- expect(result).toBe(false);
- });
-
- test('should handle JSON parsing errors gracefully', () => {
- // Set invalid JSON in cache
- const cacheKey = branchListingCacheService.getCacheKey(testOwner, testRepo);
- mockLocalStorage[cacheKey] = 'invalid json';
-
- const cached = branchListingCacheService.getCachedData(testOwner, testRepo);
- expect(cached).toBeNull();
- });
- });
-});
\ No newline at end of file
diff --git a/src/tests/BranchListing.test.js b/src/tests/BranchListing.test.js
deleted file mode 100644
index ef588126a..000000000
--- a/src/tests/BranchListing.test.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
-import BranchListing from '../components/BranchListing';
-
-// Mock fetch globally
-global.fetch = jest.fn();
-
-// Mock localStorage for cache service
-const localStorageMock = {
- getItem: jest.fn(() => null),
- setItem: jest.fn(),
- removeItem: jest.fn(),
- clear: jest.fn()
-};
-global.localStorage = localStorageMock;
-
-// Mock the cache service
-jest.mock('../services/branchListingCacheService', () => ({
- getCachedData: jest.fn(() => null),
- setCachedData: jest.fn(() => true),
- getCacheInfo: jest.fn(() => ({ exists: false, stale: true })),
- forceRefresh: jest.fn(() => true)
-}));
-
-// Mock PageLayout component
-jest.mock('../components/framework', () => ({
- PageLayout: ({ children, pageName, showMascot }) => (
-
- {children}
-
- )
-}));
-
-// Mock HelpModal component
-jest.mock('../components/HelpModal', () => {
- return function MockHelpModal({ helpTopic, onClose }) {
- return (
-
-
{helpTopic.title}
- Close
-
- );
- };
-});
-
-describe('BranchListing Component', () => {
- beforeEach(() => {
- fetch.mockClear();
- });
-
- it('renders loading state initially', () => {
- // Mock fetch to never resolve to test loading state
- fetch.mockImplementation(() => new Promise(() => {}));
-
- render(
-
-
-
- );
-
- expect(screen.getByText('Loading previews...')).toBeInTheDocument();
- });
-
- it('renders deployment selection page with PR functionality', async () => {
- // Mock successful API responses
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- // Mock deployment status checks (will return HEAD requests)
- .mockResolvedValue({
- ok: true,
- status: 200
- });
-
- render(
-
-
-
- );
-
- // Wait for data to load
- await waitFor(() => {
- expect(screen.getByText(/SGEX Deployment Selection/)).toBeInTheDocument();
- expect(screen.getByText(/Pull Request Previews \(\d+\)/)).toBeInTheDocument();
- });
-
- // Check that sections are properly displayed
- expect(screen.getByText(/đ Main Branch/)).toBeInTheDocument();
- expect(screen.getByText(/Pull Request Previews \(\d+\)/)).toBeInTheDocument();
- });
-
- it('allows PR filtering', async () => {
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Add new feature',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/new-feature' },
- html_url: 'https://github.com/test/repo/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- },
- {
- id: 2,
- number: 124,
- title: 'Fix authentication bug',
- state: 'open',
- user: { login: 'developer' },
- head: { ref: 'fix/auth-bug' },
- html_url: 'https://github.com/test/repo/pull/124',
- updated_at: '2023-01-02T00:00:00Z',
- created_at: '2023-01-02T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({
- ok: true,
- status: 200
- });
-
- render(
-
-
-
- );
-
- await waitFor(() => {
- expect(screen.getByText(/Add new feature/)).toBeInTheDocument();
- expect(screen.getByText(/Fix authentication bug/)).toBeInTheDocument();
- });
-
- // Test PR filtering
- const prSearchInput = screen.getByPlaceholderText('Search pull requests by title or author...');
- fireEvent.change(prSearchInput, { target: { value: 'authentication' } });
-
- // Should still see authentication PR, but not the feature PR
- expect(screen.getByText(/Fix authentication bug/)).toBeInTheDocument();
- expect(screen.queryByText(/Add new feature/)).not.toBeInTheDocument();
- });
-
- it('handles API errors gracefully', async () => {
- fetch.mockRejectedValue(new Error('Network error'));
-
- render(
-
-
-
- );
-
- await waitFor(() => {
- expect(screen.getByText(/Failed to load previews/)).toBeInTheDocument();
- expect(screen.getByText(/Please try refreshing the page/)).toBeInTheDocument();
- });
- });
-
- it('shows contribute modal when button is clicked', async () => {
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- });
-
- render(
-
-
-
- );
-
- await waitFor(() => {
- expect(screen.getByText(/How to Contribute/)).toBeInTheDocument();
- });
-
- // Click the contribute button
- fireEvent.click(screen.getByText(/How to Contribute/));
-
- // Should show the modal
- expect(screen.getByTestId('help-modal')).toBeInTheDocument();
- expect(screen.getByText('How to Contribute to SGEX')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/tests/BranchListingCaching.test.js b/src/tests/BranchListingCaching.test.js
deleted file mode 100644
index 3fb2d788a..000000000
--- a/src/tests/BranchListingCaching.test.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/**
- * Integration test for BranchListing caching functionality
- */
-
-import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
-import BranchListing from '../components/BranchListing';
-import branchListingCacheService from '../services/branchListingCacheService';
-
-// Mock GitHub API responses
-const mockPRData = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'test-branch' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: new Date().toISOString(),
- created_at: new Date(Date.now() - 86400000).toISOString()
- }
-];
-
-const mockBranchData = [
- {
- name: 'main',
- commit: {
- sha: 'abc123',
- commit: {
- committer: {
- date: new Date().toISOString()
- }
- }
- }
- }
-];
-
-// Mock fetch
-global.fetch = jest.fn();
-
-// Mock GitHub Actions service
-jest.mock('../services/githubActionsService', () => ({
- setToken: jest.fn(),
- getWorkflowStatusForBranches: jest.fn(() => Promise.resolve({})),
- triggerWorkflow: jest.fn(() => Promise.resolve(true))
-}));
-
-// Mock hooks
-jest.mock('../hooks/useThemeImage', () => ({
- __esModule: true,
- default: () => '/mock-mascot.png'
-}));
-
-describe('BranchListing Caching Integration', () => {
- beforeEach(() => {
- // Clear cache before each test
- branchListingCacheService.clearAllCaches();
- jest.clearAllMocks();
-
- // Reset fetch mock
- fetch.mockClear();
- });
-
- test('should cache PR data after initial fetch', async () => {
- // Setup fetch mocks - only PR data needed now
- fetch.mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRData)
- });
-
- // Render component
- render(
-
-
-
- );
-
- // Wait for data to load
- await waitFor(() => {
- expect(screen.getByText('#123: Test PR')).toBeInTheDocument();
- });
-
- // Verify cache contains data
- const cachedData = branchListingCacheService.getCachedData('litlfred', 'sgex');
- expect(cachedData).not.toBeNull();
- expect(cachedData.pullRequests).toHaveLength(1);
- expect(cachedData.pullRequests[0].title).toBe('Test PR');
- });
-
- test('should use cached data on subsequent renders', async () => {
- // Pre-populate cache
- const testBranches = []; // Empty branches since we don't use them anymore
-
- const testPRs = [{
- id: 1,
- number: 124,
- title: 'Cached PR',
- state: 'open',
- author: 'cacheduser',
- branchName: 'cached-branch',
- safeBranchName: 'cached-branch',
- url: './cached-branch/index.html',
- prUrl: 'https://github.com/litlfred/sgex/pull/124',
- updatedAt: new Date().toLocaleDateString(),
- createdAt: new Date(Date.now() - 86400000).toLocaleDateString()
- }];
-
- branchListingCacheService.setCachedData('litlfred', 'sgex', testBranches, testPRs);
-
- // Render component
- render(
-
-
-
- );
-
- // Wait for cached data to load
- await waitFor(() => {
- expect(screen.getByText('#124: Cached PR')).toBeInTheDocument();
- });
-
- // Verify fetch was not called for PR data (using cache)
- // Note: fetch may be called for deployment status checks and comments, which is expected
- const prFetchCalls = fetch.mock.calls.filter(call =>
- call[0].includes('/pulls?state=') || call[0].includes('/repos/litlfred/sgex/pulls')
- );
- expect(prFetchCalls).toHaveLength(0);
- });
-
- test('should show cache status information', async () => {
- // Pre-populate cache with recent data
- const testBranches = []; // Empty branches
- const testPRs = [{
- id: 1,
- number: 125,
- title: 'Cache Status Test PR',
- state: 'open',
- author: 'testuser',
- branchName: 'test-cache-status',
- safeBranchName: 'test-cache-status',
- url: './test-cache-status/index.html',
- prUrl: 'https://github.com/litlfred/sgex/pull/125',
- updatedAt: new Date().toLocaleDateString(),
- createdAt: new Date().toLocaleDateString()
- }];
-
- branchListingCacheService.setCachedData('litlfred', 'sgex', testBranches, testPRs);
-
- // Render component
- render(
-
-
-
- );
-
- // Wait for cache status to appear
- await waitFor(() => {
- expect(screen.getByText(/đ Data cached/)).toBeInTheDocument();
- expect(screen.getByText(/Refresh/)).toBeInTheDocument();
- });
- });
-
- test('should expire stale cache after 5 minutes', () => {
- // Create stale cache data (6 minutes old)
- const staleTimestamp = Date.now() - (6 * 60 * 1000);
- const staleData = {
- branches: [],
- pullRequests: [],
- timestamp: staleTimestamp,
- owner: 'litlfred',
- repo: 'sgex'
- };
-
- // Manually set stale data in localStorage
- localStorage.setItem('sgex_branch_listing_cache_litlfred_sgex', JSON.stringify(staleData));
-
- // Try to get cached data
- const cachedData = branchListingCacheService.getCachedData('litlfred', 'sgex');
-
- // Should return null for stale data
- expect(cachedData).toBeNull();
-
- // Should remove stale data from localStorage
- expect(localStorage.getItem('sgex_branch_listing_cache_litlfred_sgex')).toBeNull();
- });
-
- test('should provide cache info correctly', () => {
- // Cache some test data
- const testBranches = [];
- const testPRs = [{
- id: 1,
- number: 126,
- title: 'Cache Info Test',
- state: 'open'
- }];
-
- branchListingCacheService.setCachedData('litlfred', 'sgex', testBranches, testPRs);
-
- // Get cache info
- const cacheInfo = branchListingCacheService.getCacheInfo('litlfred', 'sgex');
-
- expect(cacheInfo.exists).toBe(true);
- expect(cacheInfo.stale).toBe(false);
- expect(cacheInfo.prCount).toBe(1);
- expect(cacheInfo.ageMinutes).toBe(0);
- });
-
- test('should clear cache on force refresh', () => {
- // Cache some test data
- branchListingCacheService.setCachedData('litlfred', 'sgex', [], []);
-
- // Verify data is cached
- expect(branchListingCacheService.getCachedData('litlfred', 'sgex')).not.toBeNull();
-
- // Force refresh
- branchListingCacheService.forceRefresh('litlfred', 'sgex');
-
- // Verify cache is cleared
- expect(branchListingCacheService.getCachedData('litlfred', 'sgex')).toBeNull();
- });
-});
\ No newline at end of file
diff --git a/src/tests/BranchListingComments.test.js b/src/tests/BranchListingComments.test.js
deleted file mode 100644
index e3f97265e..000000000
--- a/src/tests/BranchListingComments.test.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
-import BranchListing from '../components/BranchListing';
-
-// Mock fetch globally
-global.fetch = jest.fn();
-
-// Mock PageLayout component
-jest.mock('../components/framework', () => ({
- PageLayout: ({ children, pageName, showMascot }) => (
-
- {children}
-
- )
-}));
-
-// Mock HelpModal component
-jest.mock('../components/HelpModal', () => {
- return function MockHelpModal({ helpTopic, onClose }) {
- return (
-
-
{helpTopic.title}
- Close
-
- );
- };
-});
-
-// Mock WorkflowStatus component
-jest.mock('../components/WorkflowStatus', () => {
- return function MockWorkflowStatus() {
- return Workflow Status
;
- };
-});
-
-describe('BranchListing Comments Feature', () => {
- beforeEach(() => {
- fetch.mockClear();
- // Clear any stored tokens
- sessionStorage.clear();
- localStorage.clear();
- });
-
- it('shows discussion summaries for unauthenticated users', async () => {
- const mockBranches = [];
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR with Comments',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- const mockComments = [
- {
- id: 1,
- user: {
- login: 'commenter1',
- avatar_url: 'https://github.com/commenter1.png'
- },
- body: 'This is a test comment',
- created_at: '2023-01-01T12:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockBranches)
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- // Mock deployment status checks
- .mockResolvedValueOnce({
- ok: true,
- status: 200
- })
- // Mock comment summary fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockComments)
- });
-
- render(
-
-
-
- );
-
- // Wait for data to load and switch to PR tab
- await waitFor(() => {
- expect(screen.getByText(/Pull Request Previews/)).toBeInTheDocument();
- });
-
- // Click on PR tab to show PRs
- fireEvent.click(screen.getByText(/Pull Request Previews/));
-
- await waitFor(() => {
- expect(screen.getByText('Test PR with Comments')).toBeInTheDocument();
- });
-
- // Should show discussion summary even without authentication
- await waitFor(() => {
- expect(screen.getByText(/đŦ/)).toBeInTheDocument();
- });
- });
-
- it('shows sign-in message for unauthenticated users in expanded discussion', async () => {
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({
- ok: true,
- json: () => Promise.resolve([]),
- status: 200
- });
-
- render(
-
-
-
- );
-
- // Wait for PR tab to be available and click it
- await waitFor(() => {
- expect(screen.getByText(/Pull Request Previews/)).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText(/Pull Request Previews/));
-
- // Wait for PR to appear and look for discussion section
- await waitFor(() => {
- expect(screen.getByText('Test PR')).toBeInTheDocument();
- });
-
- // Find and click the discussion summary bar to expand
- const discussionBar = screen.getByText(/đŦ/);
- if (discussionBar) {
- fireEvent.click(discussionBar.closest('.discussion-summary-bar'));
-
- // Should show sign-in message for unauthenticated users
- await waitFor(() => {
- expect(screen.getByText(/Sign in to add comments/)).toBeInTheDocument();
- });
- }
- });
-});
\ No newline at end of file
diff --git a/src/tests/BranchListingSearchByBranch.test.js b/src/tests/BranchListingSearchByBranch.test.js
deleted file mode 100644
index 12bffae23..000000000
--- a/src/tests/BranchListingSearchByBranch.test.js
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
-
-// Mock BranchListing component to test search functionality
-const mockPullRequests = [
- {
- 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/index.html',
- prUrl: 'https://github.com/litlfred/sgex/pull/123',
- updatedAt: '8/4/2025',
- createdAt: '8/3/2025'
- },
- {
- 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/index.html',
- prUrl: 'https://github.com/litlfred/sgex/pull/122',
- updatedAt: '8/2/2025',
- createdAt: '7/31/2025'
- },
- {
- id: 3,
- number: 121,
- title: 'Fix authentication flow',
- state: 'open',
- author: 'contributor',
- branchName: 'fix/auth-flow',
- safeBranchName: 'fix-auth-flow',
- url: './fix-auth-flow/index.html',
- prUrl: 'https://github.com/litlfred/sgex/pull/121',
- updatedAt: '8/1/2025',
- createdAt: '7/30/2025'
- }
-];
-
-// Test search filter logic
-const searchFilter = (pullRequests, searchTerm) => {
- return pullRequests.filter(pr =>
- pr.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
- pr.author.toLowerCase().includes(searchTerm.toLowerCase()) ||
- pr.branchName.toLowerCase().includes(searchTerm.toLowerCase())
- );
-};
-
-describe('BranchListing Search Functionality', () => {
- describe('Branch Name Search', () => {
- it('should filter PRs by exact branch name match', () => {
- const results = searchFilter(mockPullRequests, 'copilot/fix-459');
-
- expect(results).toHaveLength(1);
- expect(results[0].number).toBe(123);
- expect(results[0].branchName).toBe('copilot/fix-459');
- });
-
- it('should filter PRs by partial branch name match', () => {
- const results = searchFilter(mockPullRequests, 'dark-mode');
-
- expect(results).toHaveLength(1);
- expect(results[0].number).toBe(122);
- expect(results[0].branchName).toBe('feature/dark-mode');
- });
-
- it('should find multiple PRs when branch names contain the search term', () => {
- const results = searchFilter(mockPullRequests, 'fix');
-
- expect(results).toHaveLength(2);
- expect(results.map(pr => pr.number)).toContain(123); // copilot/fix-459
- expect(results.map(pr => pr.number)).toContain(121); // fix/auth-flow
- });
-
- it('should be case insensitive for branch names', () => {
- const results = searchFilter(mockPullRequests, 'FEATURE');
-
- expect(results).toHaveLength(1);
- expect(results[0].branchName).toBe('feature/dark-mode');
- });
-
- it('should search by branch name prefix', () => {
- const results = searchFilter(mockPullRequests, 'copilot');
-
- expect(results).toHaveLength(1);
- expect(results[0].branchName).toBe('copilot/fix-459');
- });
-
- it('should search by branch name suffix', () => {
- const results = searchFilter(mockPullRequests, 'auth-flow');
-
- expect(results).toHaveLength(1);
- expect(results[0].branchName).toBe('fix/auth-flow');
- });
- });
-
- describe('Combined Search (Title, Author, Branch)', () => {
- it('should find PRs matching title OR author OR branch name', () => {
- // Search for "fix" should match:
- // - PR 123: branch "copilot/fix-459" (branch match)
- // - PR 121: title "Fix authentication flow" AND branch "fix/auth-flow" (both match)
- const results = searchFilter(mockPullRequests, 'fix');
-
- expect(results).toHaveLength(2);
- expect(results.map(pr => pr.number).sort()).toEqual([121, 123]);
- });
-
- it('should maintain existing title search functionality', () => {
- const results = searchFilter(mockPullRequests, 'authentication');
-
- expect(results).toHaveLength(1);
- expect(results[0].number).toBe(121);
- expect(results[0].title).toContain('authentication');
- });
-
- it('should maintain existing author search functionality', () => {
- const results = searchFilter(mockPullRequests, 'developer');
-
- expect(results).toHaveLength(1);
- expect(results[0].number).toBe(122);
- expect(results[0].author).toBe('developer');
- });
-
- it('should return no results for non-matching search terms', () => {
- const results = searchFilter(mockPullRequests, 'nonexistent');
-
- expect(results).toHaveLength(0);
- });
-
- it('should return all PRs for empty search term', () => {
- const results = searchFilter(mockPullRequests, '');
-
- expect(results).toHaveLength(3);
- });
- });
-
- describe('Edge Cases', () => {
- it('should handle special characters in branch names', () => {
- const specialPR = {
- ...mockPullRequests[0],
- branchName: 'feature/fix-#123-bug',
- number: 999
- };
- const prList = [...mockPullRequests, specialPR];
-
- const results = searchFilter(prList, '#123');
-
- expect(results).toHaveLength(1);
- expect(results[0].number).toBe(999);
- });
-
- it('should handle slashes in branch names', () => {
- const results = searchFilter(mockPullRequests, 'feature/');
-
- expect(results).toHaveLength(1);
- expect(results[0].branchName).toBe('feature/dark-mode');
- });
-
- it('should handle dashes in branch names', () => {
- const results = searchFilter(mockPullRequests, '-mode');
-
- expect(results).toHaveLength(1);
- expect(results[0].branchName).toBe('feature/dark-mode');
- });
- });
-});
\ No newline at end of file
diff --git a/src/tests/CommentErrorHandling.test.js b/src/tests/CommentErrorHandling.test.js
deleted file mode 100644
index cb233b0a0..000000000
--- a/src/tests/CommentErrorHandling.test.js
+++ /dev/null
@@ -1,278 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
-import BranchListing from '../components/BranchListing';
-
-// Mock fetch globally
-global.fetch = jest.fn();
-
-// Mock PageLayout component
-jest.mock('../components/framework', () => ({
- PageLayout: ({ children, pageName, showMascot }) => (
-
- {children}
-
- )
-}));
-
-// Mock HelpModal component
-jest.mock('../components/HelpModal', () => {
- return function MockHelpModal({ helpTopic, onClose }) {
- return (
-
-
{helpTopic.title}
- Close
-
- );
- };
-});
-
-// Mock WorkflowStatus component
-jest.mock('../components/WorkflowStatus', () => {
- return function MockWorkflowStatus() {
- return Workflow Status
;
- };
-});
-
-describe('Comment Error Handling', () => {
- beforeEach(() => {
- fetch.mockClear();
- // Clear any stored tokens
- sessionStorage.clear();
- localStorage.clear();
- });
-
- it('shows error message when comment submission fails with 401 (authentication)', async () => {
- // Set up authentication
- sessionStorage.setItem('github_token', 'test-token');
-
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- // Mock successful initial API calls
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({
- ok: true,
- json: () => Promise.resolve([]),
- status: 200
- });
-
- render(
-
-
-
- );
-
- // Wait for PR tab and click it
- await waitFor(() => {
- expect(screen.getByText(/Pull Request Previews/)).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText(/Pull Request Previews/));
-
- // Wait for PR to appear
- await waitFor(() => {
- expect(screen.getByText('Test PR')).toBeInTheDocument();
- });
-
- // Find and click the discussion summary to expand
- const discussionBar = screen.getByText(/đŦ/);
- fireEvent.click(discussionBar.closest('.discussion-summary-bar'));
-
- // Wait for comment input to appear
- await waitFor(() => {
- expect(screen.getByPlaceholderText('Add a comment...')).toBeInTheDocument();
- });
-
- // Type a comment
- const commentInput = screen.getByPlaceholderText('Add a comment...');
- fireEvent.change(commentInput, { target: { value: 'Test comment' } });
-
- // Mock failed comment submission with 401 error
- fetch.mockResolvedValueOnce({
- ok: false,
- status: 401
- });
-
- // Click submit button
- const submitButton = screen.getByText('Add Comment');
- fireEvent.click(submitButton);
-
- // Wait for error message to appear
- await waitFor(() => {
- expect(screen.getByText(/Authentication failed. Please check your token permissions./)).toBeInTheDocument();
- });
- });
-
- it('shows error message when comment submission fails with 403 (permission denied)', async () => {
- sessionStorage.setItem('github_token', 'test-token');
-
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({
- ok: true,
- json: () => Promise.resolve([]),
- status: 200
- });
-
- render(
-
-
-
- );
-
- await waitFor(() => {
- expect(screen.getByText(/Pull Request Previews/)).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText(/Pull Request Previews/));
-
- await waitFor(() => {
- expect(screen.getByText('Test PR')).toBeInTheDocument();
- });
-
- const discussionBar = screen.getByText(/đŦ/);
- fireEvent.click(discussionBar.closest('.discussion-summary-bar'));
-
- await waitFor(() => {
- expect(screen.getByPlaceholderText('Add a comment...')).toBeInTheDocument();
- });
-
- const commentInput = screen.getByPlaceholderText('Add a comment...');
- fireEvent.change(commentInput, { target: { value: 'Test comment' } });
-
- // Mock failed comment submission with 403 error
- fetch.mockResolvedValueOnce({
- ok: false,
- status: 403
- });
-
- const submitButton = screen.getByText('Add Comment');
- fireEvent.click(submitButton);
-
- await waitFor(() => {
- expect(screen.getByText(/Permission denied. You may not have write access to this repository./)).toBeInTheDocument();
- });
- });
-
- it('clears error message when user starts typing', async () => {
- sessionStorage.setItem('github_token', 'test-token');
-
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve([])
- })
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({
- ok: true,
- json: () => Promise.resolve([]),
- status: 200
- });
-
- render(
-
-
-
- );
-
- await waitFor(() => {
- expect(screen.getByText(/Pull Request Previews/)).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText(/Pull Request Previews/));
-
- await waitFor(() => {
- expect(screen.getByText('Test PR')).toBeInTheDocument();
- });
-
- const discussionBar = screen.getByText(/đŦ/);
- fireEvent.click(discussionBar.closest('.discussion-summary-bar'));
-
- await waitFor(() => {
- expect(screen.getByPlaceholderText('Add a comment...')).toBeInTheDocument();
- });
-
- const commentInput = screen.getByPlaceholderText('Add a comment...');
- fireEvent.change(commentInput, { target: { value: 'Test comment' } });
-
- // Mock failed comment submission
- fetch.mockResolvedValueOnce({
- ok: false,
- status: 500
- });
-
- const submitButton = screen.getByText('Add Comment');
- fireEvent.click(submitButton);
-
- // Wait for error message to appear
- await waitFor(() => {
- expect(screen.getByText(/GitHub server error. Please try again later./)).toBeInTheDocument();
- });
-
- // Start typing again - error should clear
- fireEvent.change(commentInput, { target: { value: 'New comment text' } });
-
- // Error message should no longer be visible
- await waitFor(() => {
- expect(screen.queryByText(/GitHub server error. Please try again later./)).not.toBeInTheDocument();
- });
- });
-});
\ No newline at end of file
diff --git a/src/tests/preview-url-linkable.test.js b/src/tests/preview-url-linkable.test.js
deleted file mode 100644
index 964d2e893..000000000
--- a/src/tests/preview-url-linkable.test.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
-import BranchListing from '../components/BranchListing';
-
-// Mock fetch globally
-global.fetch = jest.fn();
-
-// Mock PageLayout component
-jest.mock('../components/framework', () => ({
- PageLayout: ({ children }) => {children}
-}));
-
-// Mock HelpModal component
-jest.mock('../components/HelpModal', () => {
- return function MockHelpModal({ onClose }) {
- return Close
;
- };
-});
-
-describe('BranchListing Preview URL Links', () => {
- beforeEach(() => {
- fetch.mockClear();
- });
-
- it('should make preview URLs clickable links', async () => {
- const mockPRs = [
- {
- id: 1,
- number: 123,
- title: 'Test PR',
- state: 'open',
- user: { login: 'testuser' },
- head: { ref: 'feature/test-pr' },
- html_url: 'https://github.com/litlfred/sgex/pull/123',
- updated_at: '2023-01-01T00:00:00Z',
- created_at: '2023-01-01T00:00:00Z'
- }
- ];
-
- fetch
- .mockResolvedValueOnce({
- ok: true,
- json: () => Promise.resolve(mockPRs)
- })
- .mockResolvedValue({ ok: true, status: 200 });
-
- render(
-
-
-
- );
-
- // Wait for data to load and find preview URL links
- await screen.findByText(/Pull Request Previews \(\d+\)/);
-
- // Check that preview URLs in card footers are clickable links
- const previewUrlLinks = screen.getAllByText(/Preview URL:/);
-
- // Each preview URL should be within a link element or should be a clickable link
- previewUrlLinks.forEach((element) => {
- const parent = element.closest('.card-footer');
- expect(parent).toBeInTheDocument();
-
- // Look for a link within the same card footer
- const linkElement = parent.querySelector('a[href]');
- expect(linkElement).toBeInTheDocument();
- expect(linkElement.getAttribute('href')).toMatch(/\.\/.*\/index\.html/);
- });
- });
-});
\ No newline at end of file
From 98c8dece8938e86f1fc78aa7bb0d76f12a844dd9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 13:04:31 +0000
Subject: [PATCH 4/4] Fix deployment workflows to preserve existing branch
directories
- Modified landing page deployment to only remove files, never directories
- Added safety validation to branch preview deployment
- Added fallback logic for missing deploy branch
- Improved backup and restore logic to prevent removal of existing builds like main/
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
---
.github/workflows/landing-page-deploy.yml | 114 +++++++++++++++-------
.github/workflows/pages.yml | 55 ++++++++++-
2 files changed, 130 insertions(+), 39 deletions(-)
diff --git a/.github/workflows/landing-page-deploy.yml b/.github/workflows/landing-page-deploy.yml
index db7555c4b..2b0498bca 100644
--- a/.github/workflows/landing-page-deploy.yml
+++ b/.github/workflows/landing-page-deploy.yml
@@ -28,10 +28,10 @@ jobs:
deploy-landing-page:
runs-on: ubuntu-latest
steps:
- - name: Checkout deploy branch
+ - name: Checkout deploy branch (or fallback to main)
uses: actions/checkout@v4
with:
- ref: ${{ github.event.inputs.source_branch || 'deploy' }}
+ ref: main # Use main branch since deploy branch doesn't exist yet
fetch-depth: 0 # Fetch full history for gh-pages branch
- name: Configure git user
@@ -54,10 +54,56 @@ jobs:
run: |
set -e
- echo "Building self-contained landing page from deploy branch..."
+ echo "Building self-contained landing page..."
- # Build the landing page with self-contained assets
- npm run build:landing
+ # Check if we have landing page assets from deploy branch
+ if [[ -f "public/branch-listing.html" ]]; then
+ echo "Using landing page assets from deploy branch..."
+ npm run build:landing
+ else
+ echo "Deploy branch assets not available, creating basic landing page..."
+
+ # Create a minimal landing page build
+ mkdir -p build
+
+ cat > build/index.html << 'EOF'
+
+
+
+
+
+ SGEX Workbench
+
+
+
+
+
đ SGEX Workbench
+
WHO SMART Guidelines Exchange
+
+
+
+
+
đ Branch & PR Previews
+
Branch and pull request previews are automatically deployed to subdirectories.
+
Each branch is accessible at /branch-name/
+
+
+
+
âšī¸ About
+
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 (DAKs).
+
+
+
+
+EOF
+ fi
# Verify index.html exists
if [[ ! -f "build/index.html" ]]; then
@@ -67,7 +113,7 @@ jobs:
echo "â
Landing page build completed"
- - name: Deploy to gh-pages root
+ - name: Deploy to gh-pages root (preserving existing branches)
shell: bash
run: |
set -e
@@ -93,43 +139,43 @@ jobs:
git push origin gh-pages
fi
- # Preserve all existing branch directories (exclude problematic cache directories)
- echo "Preserving existing branch directories..."
- if ls -d */ > /dev/null 2>&1; then
- mkdir -p /tmp/branch-backup
- for dir in */; do
- if [[ "$dir" != "node_modules/" && "$dir" != ".cache/" ]]; then
- echo "Backing up directory: $dir"
- # Exclude cache subdirectories when copying
- rsync -av --exclude='*/node_modules/.cache' --exclude='*/.cache' "$dir" /tmp/branch-backup/
- fi
- done
+ # CRITICAL: Only remove files, NEVER remove directories (to preserve branch builds)
+ echo "Removing only root-level files (preserving all branch directories)..."
+
+ # List what directories exist for safety verification
+ echo "Existing directories before cleanup:"
+ ls -la */ 2>/dev/null || echo "No directories found"
+
+ # Remove only specific files that could conflict with landing page
+ rm -f index.html
+ rm -f manifest.json
+ rm -f asset-manifest.json
+ rm -f service-worker.js
+ rm -f robots.txt
+ rm -f favicon.ico
+ rm -f logo*.png
+ rm -f README.md
+ rm -f package.json
+ rm -f package-lock.json
+
+ # Remove static directory only if it exists at root (not in branch subdirs)
+ if [[ -d "static" ]]; then
+ echo "Removing root static directory..."
+ rm -rf static
fi
- # Clear root (except .git and branch directories)
- echo "Clearing root directory for landing page deployment..."
- find . -maxdepth 1 -not -name '.' -not -name '.git' -not -name '.github' -exec rm -rf {} +
-
- # Also clean up any cache directories that might exist
- echo "Cleaning up cache directories..."
- find . -name "node_modules/.cache" -type d -exec rm -rf {} + 2>/dev/null || true
- find . -name ".cache" -type d -exec rm -rf {} + 2>/dev/null || true
-
- # Deploy landing page to root
+ # Deploy landing page to root (this will not affect any subdirectories)
echo "Deploying landing page to root..."
cp -a /tmp/landing-build/. .
- # Restore all branch directories
- if [[ -d /tmp/branch-backup ]]; then
- echo "Restoring branch directories..."
- cp -r /tmp/branch-backup/* .
- rm -rf /tmp/branch-backup
- fi
+ # Verify existing directories are still there
+ echo "Directories after deployment:"
+ ls -la */ 2>/dev/null || echo "No directories found"
# Clean up temporary build
rm -rf /tmp/landing-build
- echo "â
Landing page deployed to gh-pages root"
+ echo "â
Landing page deployed to gh-pages root while preserving all branch directories"
- name: Commit and push changes
shell: bash
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index fc559f7c9..d52228024 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -74,10 +74,41 @@ jobs:
fi
echo "Building with BASE_PATH: $BASE_PATH"
- # Get build script from deploy branch
+ # Get build script from deploy branch (fallback to local if deploy branch doesn't exist)
echo "Fetching build script from deploy branch..."
- git fetch origin deploy
- git checkout origin/deploy -- scripts/build-multi-branch.js
+ if git fetch origin deploy 2>/dev/null; then
+ echo "Deploy branch found, using build script from deploy branch"
+ git checkout origin/deploy -- scripts/build-multi-branch.js
+ else
+ echo "Deploy branch not found, checking if build script exists locally..."
+ if [[ ! -f "scripts/build-multi-branch.js" ]]; then
+ echo "ERROR: Build script not found and deploy branch doesn't exist"
+ echo "Creating minimal build script as fallback..."
+ mkdir -p scripts
+ cat > scripts/build-multi-branch.js << 'EOF'
+const { execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+console.log('Building React app for deployment...');
+
+// Simplified build process
+try {
+ // Set environment variables
+ process.env.NODE_ENV = 'production';
+ process.env.PUBLIC_URL = process.env.BASE_PATH || '/';
+
+ // Run the build
+ execSync('npm run build', { stdio: 'inherit' });
+
+ console.log('â
Build completed successfully');
+} catch (error) {
+ console.error('â Build failed:', error.message);
+ process.exit(1);
+}
+EOF
+ fi
+ fi
# Run the build script
node scripts/build-multi-branch.js branch
@@ -231,10 +262,12 @@ jobs:
branch_dir="${{ steps.validate_branch.outputs.branch_dir }}"
repo_root="$(pwd)"
+ target_subdir="${{ steps.validate_branch.outputs.target_subdir }}"
echo "Cleaning old deployment"
echo "Branch directory: $branch_dir"
echo "Repository root: $repo_root"
+ echo "Target subdirectory: $target_subdir"
# Validate before any destructive operations
if [[ "$branch_dir" != "$repo_root"* ]] || [[ ${#branch_dir} -le ${#repo_root} ]]; then
@@ -242,10 +275,22 @@ jobs:
exit 1
fi
- # Use git to safely remove the branch directory
+ # Additional safety: ensure we're only removing the specific branch directory
+ if [[ "$target_subdir" == "." || "$target_subdir" == ".." || -z "$target_subdir" ]]; then
+ echo "ERROR: Invalid target subdirectory: '$target_subdir'"
+ exit 1
+ fi
+
+ # Use git to safely remove the specific branch directory only
if [[ -d "$branch_dir" ]]; then
echo "Removing existing deployment: $branch_dir"
- git rm -rf "$branch_dir" || echo "Directory didn't exist in git"
+ # Additional safety: only remove if it matches our expected pattern
+ if [[ "$branch_dir" == "$repo_root/$target_subdir" ]]; then
+ git rm -rf "$target_subdir" 2>/dev/null || echo "Directory didn't exist in git"
+ else
+ echo "ERROR: Branch directory path doesn't match expected pattern"
+ exit 1
+ fi
else
echo "No existing deployment to clean"
fi