Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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 },
);
});
});
});
28 changes: 24 additions & 4 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 { useDispatch, 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 @@ -311,6 +312,8 @@ function PRGradingDashboard() {
day: 'numeric',
});
const selectedMockWeekMeta = MOCK_WEEK_METADATA[selectedMockWeek];
// Keep the table, summary, and CSV on the same selected week's rows.
const displayedGradings = gradings;

if (loading) {
return (
Expand All @@ -333,7 +336,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 for the selected review week."
>
Export to CSV
</button>
<div className={styles.mockWeekControl}>
<label htmlFor="mock-week-selector" className={styles.mockWeekLabel}>
Review Week
Expand All @@ -352,7 +372,7 @@ function PRGradingDashboard() {
</select>
<p className={styles.mockWeekMeta}>{selectedMockWeekMeta.range}</p>
</div>
</div>
</fieldset>
</div>
</div>

Expand All @@ -371,7 +391,7 @@ function PRGradingDashboard() {
</div>
)}
<GradingTable
gradings={gradings}
gradings={displayedGradings}
onUpdatePRsReviewed={updatePRsReviewed}
onAddPRClick={setOpenAddModal}
openAddModal={openAddModal}
Expand All @@ -387,7 +407,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