Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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', () => ({
useDispatch: () => () => false,
useSelector: selector => selector({ theme: { darkMode: false }, auth: { user: null } }),
}));
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 after selecting a review week', async () => {
render(<PRGradingDashboard />);

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(await screen.findByRole('button', { name: 'Export to CSV' }));

await waitFor(() => {
expect(downloadPRGradingCsv).toHaveBeenCalledExactlyOnceWith(
[
{
...gradings[0],
gradedPrs: [{ ...gradings[0].gradedPrs[0], isNew: false }],
},
],
'team-alpha-pr-grading-displayed-data.csv',
{ useStoredCount: true },
);
});
});
});
30 changes: 25 additions & 5 deletions src/components/PRGradingDashboard/PRGradingDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,7 +49,7 @@ function PRGradingDashboard() {

// Fetch data on mount
useEffect(() => {
fetchGradings();
void fetchGradings();
}, []);

const fetchGradings = async () => {
Expand Down Expand Up @@ -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 (
Expand All @@ -253,7 +256,24 @@ function PRGradingDashboard() {
<h1 className={styles.title}>{TEAM_NAME}</h1>
<p className={styles.date}>{currentDate}</p>
</div>
<div className={styles.headerControls}>
<fieldset className={styles.headerControls}>
<legend className={styles.headerControlsLegend}>Review week actions</legend>
<button
type="button"
onClick={() =>
downloadPRGradingCsv(
displayedGradings,
'team-alpha-pr-grading-displayed-data.csv',
{
useStoredCount: true,
},
)
}
className={styles.exportButton}
title="Exports the rows displayed below; the Review Week selector does not filter grading records yet."
>
Export to CSV
</button>
<div className={styles.mockWeekControl}>
<label htmlFor="mock-week-selector" className={styles.mockWeekLabel}>
Review Week
Expand All @@ -272,7 +292,7 @@ function PRGradingDashboard() {
</select>
<p className={styles.mockWeekMeta}>{selectedMockWeekMeta.range}</p>
</div>
</div>
</fieldset>
</div>
</div>

Expand All @@ -289,7 +309,7 @@ function PRGradingDashboard() {
</button>
</div>
<GradingTable
gradings={gradings}
gradings={displayedGradings}
onUpdatePRsReviewed={updatePRsReviewed}
onAddPRClick={setOpenAddModal}
openAddModal={openAddModal}
Expand All @@ -302,7 +322,7 @@ function PRGradingDashboard() {
<div className={styles.summarySection}>
<h2 className={styles.summaryTitle}>Summary</h2>
<SummaryList
gradings={gradings}
gradings={displayedGradings}
onUpdateGrade={updateGrade}
onRemovePR={removeGradedPR}
/>
Expand Down
49 changes: 49 additions & 0 deletions src/components/PRGradingDashboard/PRGradingDashboard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@
display: flex;
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 {
Expand Down Expand Up @@ -97,6 +112,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;
Expand Down Expand Up @@ -254,6 +286,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;
}
Expand Down Expand Up @@ -289,10 +331,17 @@

.headerControls {
width: 100%;
align-items: stretch;
flex-direction: column;
}

.mockWeekControl {
width: 100%;
order: -1;
}

.exportButton {
align-self: stretch;
}

.mockWeekMeta {
Expand Down
67 changes: 67 additions & 0 deletions src/components/PRGradingScreen/PRGradingScreen.export.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { fireEvent, render, screen, within } 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(<PRGradingScreen teamData={teamData} reviewers={reviewers} />);

const headerActions = within(screen.getByRole('group', { name: 'Grading actions' }));
expect(headerActions.getByRole('button', { name: 'Export to CSV' })).toBeInTheDocument();
expect(headerActions.getByRole('button', { name: 'Done' })).toBeInTheDocument();
expect(screen.getAllByRole('button', { name: /Promote/ })).toHaveLength(reviewers.length);

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(<PRGradingScreen teamData={teamData} reviewers={reviewers} />);

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,
);
});
});
5 changes: 5 additions & 0 deletions src/components/PRGradingScreen/PRGradingScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
import PRGradingView from './PRGradingView';
import { usePRPromotion } from './usePRPromotion';
import { downloadWeeklyPRGradingCsv } from './csvExport';

const PRGradingScreen = ({ teamData, reviewers }) => {
const darkMode = useSelector(state => state.theme.darkMode);
Expand Down Expand Up @@ -109,6 +110,9 @@ const PRGradingScreen = ({ teamData, reviewers }) => {

const handleCloseGradingModal = () => setShowGradingModal(null);
const handleFinalize = () => setIsFinalized(true);
const handleExport = () => {
downloadWeeklyPRGradingCsv(filteredReviewers, teamData.dateRange);
};

if (!teamData || !reviewers) {
return <div>Error: Missing required props</div>;
Expand All @@ -120,6 +124,7 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
teamData={teamData}
isFinalized={isFinalized}
onFinalize={handleFinalize}
onExport={handleExport}
searchTerm={searchTerm}
onSearchTermChange={setSearchTerm}
roleFilter={roleFilter}
Expand Down
32 changes: 30 additions & 2 deletions src/components/PRGradingScreen/PRGradingScreen.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -536,6 +553,12 @@
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;
align-self: flex-end;
Expand Down Expand Up @@ -700,6 +723,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;
Expand Down Expand Up @@ -1157,7 +1184,8 @@
}

.pr-grading-inline-header-cell.dark-mode {
color: #bbb;
background-color: #2d4059;
color: #fff;
border-bottom-color: rgb(255 255 255 / 10%);
}

Expand Down Expand Up @@ -1247,4 +1275,4 @@
padding: 2px 6px;
font-size: 12px;
}
}
}
Loading
Loading