From a8af74c63437bb017cd35f1aca6f8780e814c807 Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Fri, 2 Oct 2026 16:05:51 -0700
Subject: [PATCH 1/9] Add CSV export to weekly PR grading screen
---
.../PRGradingScreen.export.test.jsx | 62 +++++++++++++++++++
.../PRGradingScreen/PRGradingScreen.jsx | 25 +++++---
.../PRGradingScreen.module.css | 14 ++++-
src/components/PRGradingScreen/csvExport.js | 57 +++++++++++++++++
.../PRGradingScreen/csvExport.test.js | 32 ++++++++++
5 files changed, 181 insertions(+), 9 deletions(-)
create mode 100644 src/components/PRGradingScreen/PRGradingScreen.export.test.jsx
create mode 100644 src/components/PRGradingScreen/csvExport.js
create mode 100644 src/components/PRGradingScreen/csvExport.test.js
diff --git a/src/components/PRGradingScreen/PRGradingScreen.export.test.jsx b/src/components/PRGradingScreen/PRGradingScreen.export.test.jsx
new file mode 100644
index 00000000000..dcaf36c21dd
--- /dev/null
+++ b/src/components/PRGradingScreen/PRGradingScreen.export.test.jsx
@@ -0,0 +1,62 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import PRGradingScreen from './PRGradingScreen';
+import { downloadWeeklyPRGradingCsv } from './csvExport';
+
+vi.mock('react-redux', () => ({
+ useSelector: selector => selector({ theme: { darkMode: false } }),
+}));
+
+vi.mock('./csvExport', () => ({
+ downloadWeeklyPRGradingCsv: vi.fn(),
+}));
+
+const dateRange = { start: '5/18/2025', end: '5/25/2025' };
+const teamData = { teamName: '91NePRT', dateRange };
+const reviewers = [
+ { id: '1', reviewer: 'Alice', role: 'Mentor', prsNeeded: 2, gradedPrs: [] },
+ { id: '2', reviewer: 'Bob', role: 'Reviewer', prsNeeded: 1, gradedPrs: [] },
+ { id: '3', reviewer: 'Alex', role: 'Reviewer', prsNeeded: 1, gradedPrs: [] },
+];
+
+describe('weekly PR grading Export to CSV button', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('exports only reviewers matching the current search and role filters', () => {
+ render();
+
+ fireEvent.change(screen.getByPlaceholderText('Search reviewers by name...'), {
+ target: { value: 'al' },
+ });
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'Mentor' } });
+ fireEvent.click(screen.getByRole('button', { name: 'Export to CSV' }));
+
+ expect(downloadWeeklyPRGradingCsv).toHaveBeenCalledExactlyOnceWith([reviewers[0]], dateRange);
+ });
+
+ it('exports a newly added PR without requiring Done', () => {
+ render();
+
+ fireEvent.click(screen.getAllByRole('button', { name: '+ Add new' })[0]);
+ fireEvent.change(screen.getByPlaceholderText('1070 or 1070 + 1256'), {
+ target: { value: '1234' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Add' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Export to CSV' }));
+
+ expect(downloadWeeklyPRGradingCsv).toHaveBeenCalledExactlyOnceWith(
+ [
+ expect.objectContaining({
+ reviewer: 'Alice',
+ prsReviewed: 1,
+ gradedPrs: [expect.objectContaining({ prNumbers: '1234', grade: 'Okay' })],
+ }),
+ reviewers[1],
+ reviewers[2],
+ ],
+ dateRange,
+ );
+ });
+});
diff --git a/src/components/PRGradingScreen/PRGradingScreen.jsx b/src/components/PRGradingScreen/PRGradingScreen.jsx
index 4c96cf1d2f4..bd758e49aad 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.jsx
+++ b/src/components/PRGradingScreen/PRGradingScreen.jsx
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
import { useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
+import { downloadWeeklyPRGradingCsv } from './csvExport';
import styles from './PRGradingScreen.module.css';
const PRGradingScreen = ({ teamData, reviewers }) => {
@@ -112,6 +113,9 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const handleCloseGradingModal = () => setShowGradingModal(null);
const handleFinalize = () => setIsFinalized(true);
+ const handleExport = () => {
+ downloadWeeklyPRGradingCsv(filteredReviewers, teamData.dateRange);
+ };
/* ---------------- RENDER ---------------- */
@@ -132,14 +136,19 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
{teamData.teamName} - {teamData.dateRange.start} to {teamData.dateRange.end}
-
+
+
+
+
diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css
index 0f844c96917..593be3e5e75 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.module.css
+++ b/src/components/PRGradingScreen/PRGradingScreen.module.css
@@ -35,6 +35,12 @@
align-items: flex-start;
}
+.pr-grading-screen-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
.pr-grading-screen-header-left {
flex: 1;
}
@@ -538,6 +544,12 @@
flex-direction: column;
gap: 15px;
}
+
+ .pr-grading-screen-header-actions {
+ align-self: flex-end;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }
.pr-grading-screen-header-right {
margin-left: 0;
@@ -1006,4 +1018,4 @@
.pr-grading-screen-no-results.dark-mode {
color: #9ab0cc;
-}
\ No newline at end of file
+}
diff --git a/src/components/PRGradingScreen/csvExport.js b/src/components/PRGradingScreen/csvExport.js
new file mode 100644
index 00000000000..797e71ce594
--- /dev/null
+++ b/src/components/PRGradingScreen/csvExport.js
@@ -0,0 +1,57 @@
+const CSV_HEADERS = ['Reviewer Name', 'PR Reviewed Count', 'PRs Needed', 'PR Numbers', 'Notes'];
+
+const protectSpreadsheetValue = value => {
+ const text = String(value ?? '');
+ return /^[\t\r ]*[=+\-@]/.test(text) ? `'${text}` : text;
+};
+
+const escapeCsvValue = value => `"${protectSpreadsheetValue(value).replace(/"/g, '""')}"`;
+
+const getNotes = reviewer => {
+ const reviewerNotes = reviewer.notes ?? reviewer.note;
+ if (reviewerNotes) return reviewerNotes;
+
+ return reviewer.gradedPrs
+ .map(pr => pr.notes ?? pr.note)
+ .filter(Boolean)
+ .join('; ');
+};
+
+export const buildWeeklyPRGradingCsv = reviewers => {
+ const rows = reviewers.map(reviewer => [
+ reviewer.reviewer,
+ reviewer.gradedPrs.length,
+ reviewer.prsNeeded ?? '',
+ reviewer.gradedPrs.map(pr => pr.prNumbers).join('; '),
+ getNotes(reviewer),
+ ]);
+
+ return [CSV_HEADERS, ...rows].map(row => row.map(escapeCsvValue).join(',')).join('\r\n');
+};
+
+const filenamePart = value =>
+ String(value)
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-|-$/g, '');
+
+export const getWeeklyPRGradingFilename = dateRange => {
+ const start = filenamePart(dateRange.start);
+ const end = filenamePart(dateRange.end);
+ return `weekly-pr-grading-${start}-to-${end}.csv`;
+};
+
+export const downloadWeeklyPRGradingCsv = (reviewers, dateRange) => {
+ const csv = buildWeeklyPRGradingCsv(reviewers);
+ const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ link.href = url;
+ link.download = getWeeklyPRGradingFilename(dateRange);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ URL.revokeObjectURL(url);
+};
diff --git a/src/components/PRGradingScreen/csvExport.test.js b/src/components/PRGradingScreen/csvExport.test.js
new file mode 100644
index 00000000000..65127eef6b5
--- /dev/null
+++ b/src/components/PRGradingScreen/csvExport.test.js
@@ -0,0 +1,32 @@
+import { describe, expect, it } from 'vitest';
+import { buildWeeklyPRGradingCsv, getWeeklyPRGradingFilename } from './csvExport';
+
+describe('weekly PR grading CSV export', () => {
+ it('exports reviewer totals, PR numbers, and notes with CSV-safe escaping', () => {
+ const csv = buildWeeklyPRGradingCsv([
+ {
+ reviewer: 'Doe, Jane',
+ prsNeeded: 3,
+ notes: 'Reviewed "carefully"',
+ gradedPrs: [
+ { prNumbers: '123', grade: 'Okay' },
+ { prNumbers: '456 + 789', grade: 'Exceptional' },
+ ],
+ },
+ {
+ reviewer: '=HYPERLINK("bad")',
+ prsNeeded: 1,
+ gradedPrs: [{ prNumbers: '321', grade: 'Okay', note: 'Follow up' }],
+ },
+ ]);
+
+ expect(csv).toContain('"Doe, Jane","2","3","123; 456 + 789","Reviewed ""carefully"""');
+ expect(csv).toContain('"\'=HYPERLINK(""bad"")","1","1","321","Follow up"');
+ });
+
+ it('creates a clear filename from the visible week date range', () => {
+ expect(getWeeklyPRGradingFilename({ start: 'Sep 21, 2026', end: 'Sep 27, 2026' })).toBe(
+ 'weekly-pr-grading-sep-21-2026-to-sep-27-2026.csv',
+ );
+ });
+});
From fb9507d0a91ca70b435a9d9f95b7d5e3ff1b0a8c Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Wed, 14 Oct 2026 22:21:40 -0700
Subject: [PATCH 2/9] Defer CSV download URL cleanup
---
src/components/PRGradingScreen/csvExport.js | 2 +-
.../PRGradingScreen/csvExport.test.js | 29 +++++++++++++++++--
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/src/components/PRGradingScreen/csvExport.js b/src/components/PRGradingScreen/csvExport.js
index 797e71ce594..15379d2b97d 100644
--- a/src/components/PRGradingScreen/csvExport.js
+++ b/src/components/PRGradingScreen/csvExport.js
@@ -53,5 +53,5 @@ export const downloadWeeklyPRGradingCsv = (reviewers, dateRange) => {
document.body.appendChild(link);
link.click();
link.remove();
- URL.revokeObjectURL(url);
+ setTimeout(() => URL.revokeObjectURL(url), 0);
};
diff --git a/src/components/PRGradingScreen/csvExport.test.js b/src/components/PRGradingScreen/csvExport.test.js
index 65127eef6b5..36fe072ed00 100644
--- a/src/components/PRGradingScreen/csvExport.test.js
+++ b/src/components/PRGradingScreen/csvExport.test.js
@@ -1,5 +1,15 @@
-import { describe, expect, it } from 'vitest';
-import { buildWeeklyPRGradingCsv, getWeeklyPRGradingFilename } from './csvExport';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+ buildWeeklyPRGradingCsv,
+ downloadWeeklyPRGradingCsv,
+ getWeeklyPRGradingFilename,
+} from './csvExport';
+
+afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
describe('weekly PR grading CSV export', () => {
it('exports reviewer totals, PR numbers, and notes with CSV-safe escaping', () => {
@@ -29,4 +39,19 @@ describe('weekly PR grading CSV export', () => {
'weekly-pr-grading-sep-21-2026-to-sep-27-2026.csv',
);
});
+
+ it('releases the temporary download URL after the click, not in the same tick', () => {
+ vi.useFakeTimers();
+ const createObjectURL = vi.fn(() => 'blob:weekly-grading');
+ const revokeObjectURL = vi.fn();
+ vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
+ const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
+
+ downloadWeeklyPRGradingCsv([], { start: '5/18/2025', end: '5/25/2025' });
+
+ expect(click).toHaveBeenCalledOnce();
+ expect(revokeObjectURL).not.toHaveBeenCalled();
+ vi.runAllTimers();
+ expect(revokeObjectURL).toHaveBeenCalledWith('blob:weekly-grading');
+ });
});
From 7d5a96026eef99efef2f543425f05f93f47cd2aa Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Fri, 16 Oct 2026 12:15:15 -0700
Subject: [PATCH 3/9] Include PR grades in CSV and fix grading dark mode
---
.../PRGradingScreen.module.css | 3 ++-
.../PRGradingScreen/PRGradingView.jsx | 2 +-
src/components/PRGradingScreen/csvExport.js | 10 ++++++-
.../PRGradingScreen/csvExport.test.js | 27 ++++++++++++++++---
4 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css
index b2d27a8c1ed..5666c641d78 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.module.css
+++ b/src/components/PRGradingScreen/PRGradingScreen.module.css
@@ -1163,7 +1163,8 @@
}
.pr-grading-inline-header-cell.dark-mode {
- color: #bbb;
+ background-color: #2d4059;
+ color: #fff;
border-bottom-color: rgb(255 255 255 / 10%);
}
diff --git a/src/components/PRGradingScreen/PRGradingView.jsx b/src/components/PRGradingScreen/PRGradingView.jsx
index bc231c53990..6cd7e7d9e13 100644
--- a/src/components/PRGradingScreen/PRGradingView.jsx
+++ b/src/components/PRGradingScreen/PRGradingView.jsx
@@ -35,7 +35,7 @@ const PRGradingView = ({
gradingReviewer,
onGradeChange,
}) => {
- const dm = darkMode ? 'dark-mode' : '';
+ const dm = darkMode ? styles['dark-mode'] : '';
return (
diff --git a/src/components/PRGradingScreen/csvExport.js b/src/components/PRGradingScreen/csvExport.js
index 15379d2b97d..57ed75cecfc 100644
--- a/src/components/PRGradingScreen/csvExport.js
+++ b/src/components/PRGradingScreen/csvExport.js
@@ -1,4 +1,11 @@
-const CSV_HEADERS = ['Reviewer Name', 'PR Reviewed Count', 'PRs Needed', 'PR Numbers', 'Notes'];
+const CSV_HEADERS = [
+ 'Reviewer Name',
+ 'PR Reviewed Count',
+ 'PRs Needed',
+ 'PR Numbers',
+ 'Grades',
+ 'Notes',
+];
const protectSpreadsheetValue = value => {
const text = String(value ?? '');
@@ -23,6 +30,7 @@ export const buildWeeklyPRGradingCsv = reviewers => {
reviewer.gradedPrs.length,
reviewer.prsNeeded ?? '',
reviewer.gradedPrs.map(pr => pr.prNumbers).join('; '),
+ reviewer.gradedPrs.map(pr => pr.grade ?? '').join('; '),
getNotes(reviewer),
]);
diff --git a/src/components/PRGradingScreen/csvExport.test.js b/src/components/PRGradingScreen/csvExport.test.js
index 36fe072ed00..578f2d7b982 100644
--- a/src/components/PRGradingScreen/csvExport.test.js
+++ b/src/components/PRGradingScreen/csvExport.test.js
@@ -12,7 +12,7 @@ afterEach(() => {
});
describe('weekly PR grading CSV export', () => {
- it('exports reviewer totals, PR numbers, and notes with CSV-safe escaping', () => {
+ it('exports reviewer totals, PR numbers, aligned grades, and notes with CSV-safe escaping', () => {
const csv = buildWeeklyPRGradingCsv([
{
reviewer: 'Doe, Jane',
@@ -30,8 +30,29 @@ describe('weekly PR grading CSV export', () => {
},
]);
- expect(csv).toContain('"Doe, Jane","2","3","123; 456 + 789","Reviewed ""carefully"""');
- expect(csv).toContain('"\'=HYPERLINK(""bad"")","1","1","321","Follow up"');
+ expect(csv).toContain(
+ '"Reviewer Name","PR Reviewed Count","PRs Needed","PR Numbers","Grades","Notes"',
+ );
+ expect(csv).toContain(
+ '"Doe, Jane","2","3","123; 456 + 789","Okay; Exceptional","Reviewed ""carefully"""',
+ );
+ expect(csv).toContain('"\'=HYPERLINK(""bad"")","1","1","321","Okay","Follow up"');
+ });
+
+ it('keeps a blank grade position aligned with its PR number', () => {
+ const csv = buildWeeklyPRGradingCsv([
+ {
+ reviewer: 'Abi',
+ prsNeeded: 3,
+ gradedPrs: [
+ { prNumbers: '1000', grade: 'Exceptional' },
+ { prNumbers: '2000' },
+ { prNumbers: '3000', grade: 'Unsatisfactory' },
+ ],
+ },
+ ]);
+
+ expect(csv).toContain('"1000; 2000; 3000","Exceptional; ; Unsatisfactory"');
});
it('creates a clear filename from the visible week date range', () => {
From 443413b3278fc8f1d0bcccc133a0eeedf415b8fa Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Sun, 18 Oct 2026 17:43:02 -0700
Subject: [PATCH 4/9] Add CSV export to PR grading dashboard
---
.../PRGradingDashboard.export.test.jsx | 57 +++++++++++++++++++
.../PRGradingDashboard/PRGradingDashboard.jsx | 25 +++++++-
.../PRGradingDashboard.module.css | 35 ++++++++++++
src/components/PRGradingScreen/csvExport.js | 13 +++--
.../PRGradingScreen/csvExport.test.js | 18 ++++++
5 files changed, 140 insertions(+), 8 deletions(-)
create mode 100644 src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
new file mode 100644
index 00000000000..9d955f56b39
--- /dev/null
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
@@ -0,0 +1,57 @@
+import axios from 'axios';
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { downloadPRGradingCsv } from '../PRGradingScreen/csvExport';
+import PRGradingDashboard from './PRGradingDashboard';
+
+vi.mock('axios');
+vi.mock('react-redux', () => ({
+ useSelector: selector => selector({ theme: { darkMode: false } }),
+}));
+vi.mock('../PRGradingScreen/csvExport', () => ({
+ downloadPRGradingCsv: vi.fn(),
+}));
+
+const gradings = [
+ {
+ reviewer: 'Alice',
+ prsReviewed: 7,
+ prsNeeded: 10,
+ gradedPrs: [{ prNumbers: '1070 + 1256', grade: 'Exceptional' }],
+ },
+];
+
+describe('Team Alpha dashboard CSV export', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ axios.get.mockResolvedValue({ data: gradings });
+ });
+
+ it('exports the displayed grading data without labeling it as the selected mock week', async () => {
+ render();
+
+ const exportButton = await screen.findByRole('button', {
+ name: 'Export to CSV',
+ });
+ expect(
+ within(screen.getByRole('group', { name: 'Review week actions' })).getByRole('button', {
+ name: 'Export to CSV',
+ }),
+ ).toBe(exportButton);
+ fireEvent.change(screen.getByLabelText('Review Week'), { target: { value: '2 Weeks Ago' } });
+ fireEvent.click(exportButton);
+
+ await waitFor(() => {
+ expect(downloadPRGradingCsv).toHaveBeenCalledExactlyOnceWith(
+ [
+ {
+ ...gradings[0],
+ gradedPrs: [{ ...gradings[0].gradedPrs[0], isNew: false }],
+ },
+ ],
+ 'team-alpha-pr-grading-displayed-data.csv',
+ { useStoredCount: true },
+ );
+ });
+ });
+});
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
index 82e8a9afe38..f7cf56752fa 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.jsx
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
@@ -2,6 +2,7 @@ import axios from 'axios';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
+import { downloadPRGradingCsv } from '../PRGradingScreen/csvExport';
import AddReviewerModal from './AddReviewerModal';
import ConfirmationModal from './ConfirmationModal';
import GradingTable from './GradingTable';
@@ -231,6 +232,8 @@ function PRGradingDashboard() {
day: 'numeric',
});
const selectedMockWeekMeta = MOCK_WEEK_METADATA[selectedMockWeek];
+ // Apply future week filtering here so the table, summary, and CSV use the same rows.
+ const displayedGradings = gradings;
if (loading) {
return (
@@ -253,7 +256,23 @@ function PRGradingDashboard() {
{TEAM_NAME}
{currentDate}
-
+
+
Summary
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.module.css b/src/components/PRGradingDashboard/PRGradingDashboard.module.css
index 5c33d26c3bc..761ec95ad80 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.module.css
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.module.css
@@ -33,6 +33,7 @@
display: flex;
justify-content: flex-end;
align-items: flex-start;
+ gap: 0.75rem;
}
.mockWeekControl {
@@ -97,6 +98,23 @@
margin-bottom: 1rem;
}
+.exportButton {
+ align-self: center;
+ padding: 0.5rem 1rem;
+ background-color: transparent;
+ color: #1d4ed8;
+ font-size: 0.875rem;
+ font-weight: 600;
+ border: 1px solid #2563eb;
+ border-radius: 0.375rem;
+ cursor: pointer;
+}
+
+.exportButton:hover,
+.exportButton:focus-visible {
+ background-color: #eff6ff;
+}
+
.addReviewerButton {
padding: 0.5rem 1rem;
background-color: #2563eb;
@@ -254,6 +272,16 @@
border-color: #2563eb;
}
+:global(.dark-mode) .exportButton {
+ color: #93c5fd;
+ border-color: #60a5fa;
+}
+
+:global(.dark-mode) .exportButton:hover,
+:global(.dark-mode) .exportButton:focus-visible {
+ background-color: #1e3a5f;
+}
+
:global(.dark-mode) .addReviewerButton:hover {
background-color: #1d4ed8;
}
@@ -289,10 +317,17 @@
.headerControls {
width: 100%;
+ align-items: stretch;
+ flex-direction: column;
}
.mockWeekControl {
width: 100%;
+ order: -1;
+ }
+
+ .exportButton {
+ align-self: stretch;
}
.mockWeekMeta {
diff --git a/src/components/PRGradingScreen/csvExport.js b/src/components/PRGradingScreen/csvExport.js
index 57ed75cecfc..1417308edf2 100644
--- a/src/components/PRGradingScreen/csvExport.js
+++ b/src/components/PRGradingScreen/csvExport.js
@@ -24,10 +24,10 @@ const getNotes = reviewer => {
.join('; ');
};
-export const buildWeeklyPRGradingCsv = reviewers => {
+export const buildWeeklyPRGradingCsv = (reviewers, { useStoredCount = false } = {}) => {
const rows = reviewers.map(reviewer => [
reviewer.reviewer,
- reviewer.gradedPrs.length,
+ useStoredCount ? reviewer.prsReviewed ?? reviewer.gradedPrs.length : reviewer.gradedPrs.length,
reviewer.prsNeeded ?? '',
reviewer.gradedPrs.map(pr => pr.prNumbers).join('; '),
reviewer.gradedPrs.map(pr => pr.grade ?? '').join('; '),
@@ -50,16 +50,19 @@ export const getWeeklyPRGradingFilename = dateRange => {
return `weekly-pr-grading-${start}-to-${end}.csv`;
};
-export const downloadWeeklyPRGradingCsv = (reviewers, dateRange) => {
- const csv = buildWeeklyPRGradingCsv(reviewers);
+export const downloadPRGradingCsv = (reviewers, filename, options = {}) => {
+ const csv = buildWeeklyPRGradingCsv(reviewers, options);
const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
- link.download = getWeeklyPRGradingFilename(dateRange);
+ link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
};
+
+export const downloadWeeklyPRGradingCsv = (reviewers, dateRange) =>
+ downloadPRGradingCsv(reviewers, getWeeklyPRGradingFilename(dateRange));
diff --git a/src/components/PRGradingScreen/csvExport.test.js b/src/components/PRGradingScreen/csvExport.test.js
index 578f2d7b982..1bbaa243ef6 100644
--- a/src/components/PRGradingScreen/csvExport.test.js
+++ b/src/components/PRGradingScreen/csvExport.test.js
@@ -55,6 +55,24 @@ describe('weekly PR grading CSV export', () => {
expect(csv).toContain('"1000; 2000; 3000","Exceptional; ; Unsatisfactory"');
});
+ it('can export the dashboard count exactly as displayed, even with fewer listed PRs', () => {
+ const reviewers = [
+ {
+ reviewer: 'Alice',
+ prsReviewed: 7,
+ prsNeeded: 10,
+ gradedPrs: [{ prNumbers: '1070 + 1256', grade: 'Exceptional' }],
+ },
+ ];
+
+ expect(buildWeeklyPRGradingCsv(reviewers, { useStoredCount: true })).toContain(
+ '"Alice","7","10","1070 + 1256","Exceptional",""',
+ );
+ expect(buildWeeklyPRGradingCsv(reviewers)).toContain(
+ '"Alice","1","10","1070 + 1256","Exceptional",""',
+ );
+ });
+
it('creates a clear filename from the visible week date range', () => {
expect(getWeeklyPRGradingFilename({ start: 'Sep 21, 2026', end: 'Sep 27, 2026' })).toBe(
'weekly-pr-grading-sep-21-2026-to-sep-27-2026.csv',
From 094a311710f52074f0688160026cf9822c651d6b Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Thu, 22 Oct 2026 13:25:35 -0700
Subject: [PATCH 5/9] Fix PRs Needed contrast in dark mode
---
src/components/PRGradingScreen/PRGradingScreen.module.css | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css
index 5666c641d78..92a8017d1a4 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.module.css
+++ b/src/components/PRGradingScreen/PRGradingScreen.module.css
@@ -706,6 +706,10 @@
color: #fff;
}
+.pr-grading-screen-table.dark-mode .pr-grading-screen-td-needed {
+ color: #fff;
+}
+
.pr-grading-screen-table.dark-mode .pr-grading-screen-pr-input {
background-color: #2d4059;
border-color: #5a6b88;
From 7572c5d1c7e47b7e7a69ed9fa024771b646312de Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Thu, 22 Oct 2026 14:37:30 -0700
Subject: [PATCH 6/9] Make ActivityList sort test independent of date
---
.../CommunityPortal/Activities/__tests__/ActivityList.test.jsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx b/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx
index d5751c59432..f0b1639a039 100644
--- a/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx
+++ b/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx
@@ -57,6 +57,7 @@ describe('ActivityList', () => {
test('sorts activities by date (latest to earliest)', () => {
renderActivityList();
+ fireEvent.click(screen.getByLabelText(/Show Past Events:/i));
fireEvent.change(screen.getByLabelText(/Sort By:/i), {
target: { value: 'latest' },
From 2006d9ed0dc8df1b54119c2141add4b578415f9a Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Sun, 25 Oct 2026 17:21:55 -0700
Subject: [PATCH 7/9] Address Sonar findings in PR grading CSV export
---
.../PRGradingDashboard/PRGradingDashboard.jsx | 5 +++--
.../PRGradingDashboard.module.css | 14 ++++++++++++++
.../PRGradingScreen/PRGradingScreen.module.css | 17 +++++++++++++++++
.../PRGradingScreen/PRGradingView.jsx | 11 +++++------
src/components/PRGradingScreen/csvExport.js | 2 +-
5 files changed, 40 insertions(+), 9 deletions(-)
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
index f7cf56752fa..19d38797aa9 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.jsx
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
@@ -256,7 +256,8 @@ function PRGradingDashboard() {
{TEAM_NAME}
{currentDate}
-
+
-
+
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.module.css b/src/components/PRGradingDashboard/PRGradingDashboard.module.css
index 761ec95ad80..76e78458818 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.module.css
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.module.css
@@ -34,6 +34,20 @@
justify-content: flex-end;
align-items: flex-start;
gap: 0.75rem;
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+.headerControlsLegend {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
}
.mockWeekControl {
diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css
index 92a8017d1a4..1687a588590 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.module.css
+++ b/src/components/PRGradingScreen/PRGradingScreen.module.css
@@ -48,6 +48,23 @@
padding: 8px 20px;
}
+.pr-grading-screen-header-actions {
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+.pr-grading-screen-action-legend {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
.pr-grading-screen-title {
font-size: 2rem;
font-weight: bold;
diff --git a/src/components/PRGradingScreen/PRGradingView.jsx b/src/components/PRGradingScreen/PRGradingView.jsx
index 6cd7e7d9e13..6e045dd48cc 100644
--- a/src/components/PRGradingScreen/PRGradingView.jsx
+++ b/src/components/PRGradingScreen/PRGradingView.jsx
@@ -54,11 +54,10 @@ const PRGradingView = ({
-
+
+
diff --git a/src/components/PRGradingScreen/csvExport.js b/src/components/PRGradingScreen/csvExport.js
index 1417308edf2..0af47de0ebf 100644
--- a/src/components/PRGradingScreen/csvExport.js
+++ b/src/components/PRGradingScreen/csvExport.js
@@ -12,7 +12,7 @@ const protectSpreadsheetValue = value => {
return /^[\t\r ]*[=+\-@]/.test(text) ? `'${text}` : text;
};
-const escapeCsvValue = value => `"${protectSpreadsheetValue(value).replace(/"/g, '""')}"`;
+const escapeCsvValue = value => `"${protectSpreadsheetValue(value).replaceAll('"', '""')}"`;
const getNotes = reviewer => {
const reviewerNotes = reviewer.notes ?? reviewer.note;
From d980273848433170c00ec50bd3e60ee391166922 Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Sun, 25 Oct 2026 17:45:54 -0700
Subject: [PATCH 8/9] Fix dashboard export test for updated Redux hooks
---
.../PRGradingDashboard/PRGradingDashboard.export.test.jsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
index 9d955f56b39..9d323daa02e 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.export.test.jsx
@@ -6,7 +6,8 @@ import PRGradingDashboard from './PRGradingDashboard';
vi.mock('axios');
vi.mock('react-redux', () => ({
- useSelector: selector => selector({ theme: { darkMode: false } }),
+ useDispatch: () => () => false,
+ useSelector: selector => selector({ theme: { darkMode: false }, auth: { user: null } }),
}));
vi.mock('../PRGradingScreen/csvExport', () => ({
downloadPRGradingCsv: vi.fn(),
@@ -27,7 +28,7 @@ describe('Team Alpha dashboard CSV export', () => {
axios.get.mockResolvedValue({ data: gradings });
});
- it('exports the displayed grading data without labeling it as the selected mock week', async () => {
+ it('exports the displayed grading data after selecting a review week', async () => {
render();
const exportButton = await screen.findByRole('button', {
@@ -39,7 +40,7 @@ describe('Team Alpha dashboard CSV export', () => {
}),
).toBe(exportButton);
fireEvent.change(screen.getByLabelText('Review Week'), { target: { value: '2 Weeks Ago' } });
- fireEvent.click(exportButton);
+ fireEvent.click(await screen.findByRole('button', { name: 'Export to CSV' }));
await waitFor(() => {
expect(downloadPRGradingCsv).toHaveBeenCalledExactlyOnceWith(
From 1770acd1344f3b30c0326b0acbfd69e5cb928e3b Mon Sep 17 00:00:00 2001
From: Priyanka Mutha
Date: Sun, 25 Oct 2026 18:04:07 -0700
Subject: [PATCH 9/9] Mark dashboard grading fetch as intentionally unawaited
---
src/components/PRGradingDashboard/PRGradingDashboard.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
index 19d38797aa9..11cf860bbce 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.jsx
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
@@ -49,7 +49,7 @@ function PRGradingDashboard() {
// Fetch data on mount
useEffect(() => {
- fetchGradings();
+ void fetchGradings();
}, []);
const fetchGradings = async () => {