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
1,973 changes: 534 additions & 1,439 deletions package-lock.json

Large diffs are not rendered by default.

318 changes: 51 additions & 267 deletions src/components/PRGradingScreen/PRGradingScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import PropTypes from 'prop-types';
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 styles from './PRGradingScreen.module.css';
import PRGradingView from './PRGradingView';
import { usePRPromotion } from './usePRPromotion';

const PRGradingScreen = ({ teamData, reviewers }) => {
const darkMode = useSelector(state => state.theme.darkMode);

// Reviewer & grading state
const [reviewerData, setReviewerData] = useState(reviewers || []);
const [activeInput, setActiveInput] = useState(null);
const [inputValue, setInputValue] = useState('');
const [inputError, setInputError] = useState('');
const [showGradingModal, setShowGradingModal] = useState(null);
const [isFinalized, setIsFinalized] = useState(false);

// Search state
// Promotion business logic & state from hook
const {
promotingReviewer,
promotedReviewerIds,
handlePromoteClick,
handleConfirmPromotion,
handleCancelPromotion,
} = usePRPromotion(teamData?.teamName);

// Search filter state
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState('');

Expand All @@ -39,34 +48,24 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
setRoleFilter('');
};

if (!teamData || !reviewers) {
return <div>Error: Missing required props</div>;
}

/* ---------------- VALIDATION ---------------- */
/* ---------------- ADD PR HANDLERS ---------------- */

const validatePRNumber = value => {
const trimmed = value.trim();
const pattern = /^\d+(\s*\+\s*\d+)?$/;
if (!trimmed) return { isValid: false, error: 'PR number cannot be empty' };
if (!pattern.test(trimmed)) return { isValid: false, error: 'Format: 1070 or 1070 + 1256' };
return { isValid: true, error: '' };
if (!trimmed) return false;
return pattern.test(trimmed);
};

/* ---------------- ADD PR ---------------- */

const handleAddNewClick = reviewerId => {
if (isFinalized) return;
setActiveInput(reviewerId);
setInputValue('');
setInputError('');
};

const handleInputSubmit = reviewerId => {
if (isFinalized) return;
const validation = validatePRNumber(inputValue);
if (!validation.isValid) {
setInputError(validation.error);
if (!validatePRNumber(inputValue)) {
return;
}
const newPREntry = { id: uuidv4(), prNumbers: inputValue.trim(), grade: 'Okay' };
Expand All @@ -79,17 +78,15 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
);
setActiveInput(null);
setInputValue('');
setInputError('');
};

const handleCancel = () => {
const handleCancelInput = () => {
if (isFinalized) return;
setActiveInput(null);
setInputValue('');
setInputError('');
};

/* ---------------- MODAL ---------------- */
/* ---------------- MODAL HANDLERS ---------------- */

const handlePRNumberClick = reviewerId => {
if (isFinalized) return;
Expand All @@ -113,253 +110,40 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const handleCloseGradingModal = () => setShowGradingModal(null);
const handleFinalize = () => setIsFinalized(true);

/* ---------------- RENDER ---------------- */

const dm = darkMode ? styles['dark-mode'] : '';
if (!teamData || !reviewers) {
return <div>Error: Missing required props</div>;
}

return (
<Container fluid className={`${styles['pr-grading-screen-container']} ${dm}`}>
<Row>
<Col md={12}>
<Card className={`${styles['pr-grading-screen-card']} ${dm}`}>
<Card.Header className={`${styles['pr-grading-screen-header']} ${dm}`}>
<div className={styles['pr-grading-screen-header-content']}>
<div>
<h1 className={`${styles['pr-grading-screen-title']} ${dm}`}>
Weekly PR grading screen
</h1>
<div className={`${styles['pr-grading-screen-team-info-badge']} ${dm}`}>
{teamData.teamName} - {teamData.dateRange.start} to {teamData.dateRange.end}
</div>
</div>
<Button
variant={isFinalized ? 'secondary' : 'outline-dark'}
disabled={isFinalized}
onClick={handleFinalize}
className={dm}
>
{isFinalized ? 'Finalized' : 'Done'}
</Button>
</div>
</Card.Header>

<Card.Body className={dm}>
{/* ── Search Bar ── */}
<div className={`${styles['pr-grading-screen-search-bar']} ${dm}`}>
<input
type="text"
placeholder="Search reviewers by name..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className={`${styles['pr-grading-screen-search-input']} ${dm}`}
/>

{availableRoles.length > 0 && (
<select
value={roleFilter}
onChange={e => setRoleFilter(e.target.value)}
className={`${styles['pr-grading-screen-role-select']} ${dm}`}
>
<option value="">All roles</option>
{availableRoles.map(role => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
)}

{(searchTerm || roleFilter) && (
<button
onClick={handleClearSearch}
className={`${styles['pr-grading-screen-clear-btn']} ${dm}`}
>
βœ• Clear
</button>
)}
</div>

<table className={`${styles['pr-grading-screen-table']} ${dm}`}>
<thead>
<tr>
<th>Reviewer Name</th>
<th>PR reviewed</th>
<th>PRs Needed</th>
<th>PR Numbers</th>
</tr>
</thead>

<tbody>
{filteredReviewers.length === 0 ? (
<tr>
<td colSpan={4} className={`${styles['pr-grading-screen-no-results']} ${dm}`}>
No reviewers found
</td>
</tr>
) : (
filteredReviewers.map(reviewer => (
<tr key={reviewer.id}>
<td>{reviewer.reviewer}</td>

<td>
<input
type="number"
value={reviewer.gradedPrs.length}
readOnly
disabled={isFinalized}
className={`${styles['pr-grading-screen-pr-input']} ${dm}`}
/>
</td>

<td>{reviewer.prsNeeded}</td>

<td className={styles['pr-grading-screen-td-numbers']}>
{reviewer.gradedPrs.map(pr => (
<span
key={pr.id}
role="button"
tabIndex={0}
className={`${styles['pr-grading-screen-pr-number']} ${
pr.prNumbers.includes('+') ? styles['pr-grading-screen-pair'] : ''
} ${dm}`}
onClick={() => handlePRNumberClick(reviewer.id)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handlePRNumberClick(reviewer.id);
}
}}
>
{pr.prNumbers}
</span>
))}

{!isFinalized && activeInput !== reviewer.id && (
<Button
variant="success"
size="sm"
className={styles['pr-grading-screen-add-btn']}
onClick={() => handleAddNewClick(reviewer.id)}
>
+ Add new
</Button>
)}

{!isFinalized && activeInput === reviewer.id && (
<div className={styles['pr-grading-screen-input-container']}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
className={styles['pr-grading-screen-pr-number-input']}
placeholder="1070 or 1070 + 1256"
/>
<Button
variant="primary"
size="sm"
onClick={() => handleInputSubmit(reviewer.id)}
>
Add
</Button>
<Button variant="secondary" size="sm" onClick={handleCancel}>
Cancel
</Button>
</div>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</Card.Body>
</Card>
</Col>
</Row>

{showGradingModal && (
<div className={`${styles['pr-grading-screen-modal-overlay']} ${dm}`}>
<div className={`${styles['pr-grading-screen-modal']} ${dm}`}>
<div className={`${styles['pr-grading-screen-modal-header']} ${dm}`}>
<h4>Grade PR</h4>
<button
className={styles['pr-grading-screen-modal-close']}
onClick={handleCloseGradingModal}
>
Γ—
</button>
</div>

<div className={`${styles['pr-grading-screen-modal-body']} ${dm}`}>
<table className={`${styles['pr-grading-screen-grading-table']} ${dm}`}>
<thead>
<tr>
<th>PR Number</th>
<th>Exceptional</th>
<th>Okay</th>
<th>Unsatisfactory</th>
<th>Cannot find image</th>
</tr>
</thead>
<tbody>
{reviewerData
.find(r => r.id === showGradingModal)
?.gradedPrs.map(pr => (
<tr key={pr.id}>
<td>{pr.prNumbers}</td>
<td>
<input
type="checkbox"
disabled={isFinalized}
checked={pr.grade === 'Exceptional'}
onChange={() =>
handleGradeChange(showGradingModal, pr.id, 'Exceptional')
}
/>
</td>
<td>
<input
type="checkbox"
disabled={isFinalized}
checked={pr.grade === 'Okay'}
onChange={() => handleGradeChange(showGradingModal, pr.id, 'Okay')}
/>
</td>
<td>
<input
type="checkbox"
disabled={isFinalized}
checked={pr.grade === 'Unsatisfactory'}
onChange={() =>
handleGradeChange(showGradingModal, pr.id, 'Unsatisfactory')
}
/>
</td>
<td>
<input
type="checkbox"
disabled={isFinalized}
checked={pr.grade === 'Cannot find image'}
onChange={() =>
handleGradeChange(showGradingModal, pr.id, 'Cannot find image')
}
/>
</td>
</tr>
))}
</tbody>
</table>

<div className={`${styles['pr-grading-screen-modal-footer']} ${dm}`}>
<Button variant="primary" onClick={handleCloseGradingModal}>
Done
</Button>
</div>
</div>
</div>
</div>
)}
</Container>
<PRGradingView
darkMode={darkMode}
teamData={teamData}
isFinalized={isFinalized}
onFinalize={handleFinalize}
searchTerm={searchTerm}
onSearchTermChange={setSearchTerm}
roleFilter={roleFilter}
onRoleFilterChange={setRoleFilter}
availableRoles={availableRoles}
onClearSearch={handleClearSearch}
filteredReviewers={filteredReviewers}
promotedReviewerIds={promotedReviewerIds}
onPromoteClick={handlePromoteClick}
activeInput={activeInput}
inputValue={inputValue}
onInputValueChange={setInputValue}
onAddNewClick={handleAddNewClick}
onInputSubmit={handleInputSubmit}
onCancelInput={handleCancelInput}
onPRNumberClick={handlePRNumberClick}
promotingReviewer={promotingReviewer}
onConfirmPromotion={handleConfirmPromotion}
onCancelPromotion={handleCancelPromotion}
showGradingModal={showGradingModal}
onCloseGradingModal={handleCloseGradingModal}
gradingReviewer={reviewerData.find(r => r.id === showGradingModal)}
onGradeChange={handleGradeChange}
/>
);
};

Expand Down
Loading
Loading