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}

-
+
+ Review week actions
-
+ 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 = ({

-
+
+ + Grading actions + @@ -70,7 +69,7 @@ const PRGradingView = ({ > {isFinalized ? 'Finalized' : 'Done'} -
+ 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 () => {