-
Notifications
You must be signed in to change notification settings - Fork 51
Validate reviewer errors on saving challenge #1682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ import { connect } from 'react-redux' | |
import cn from 'classnames' | ||
import { PrimaryButton, OutlineButton } from '../../Buttons' | ||
import { REVIEW_OPPORTUNITY_TYPE_LABELS, REVIEW_OPPORTUNITY_TYPES, VALIDATION_VALUE_TYPE } from '../../../config/constants' | ||
import { loadScorecards, loadDefaultReviewers, loadWorkflows } from '../../../actions/challenges' | ||
import { loadScorecards, loadDefaultReviewers, loadWorkflows, loadScorecardById } from '../../../actions/challenges' | ||
import styles from './ChallengeReviewer-Field.module.scss' | ||
import { convertDollarToInteger, validateValue } from '../../../util/input-check' | ||
|
||
|
@@ -31,18 +31,28 @@ class ChallengeReviewerField extends Component { | |
} | ||
|
||
componentDidMount () { | ||
this.loadScorecards() | ||
if (this.props.readOnly) { | ||
// In read-only mode, only load specific scorecards for existing reviewers | ||
this.loadSpecificScorecards() | ||
} else { | ||
// In edit mode, load all scorecards for dropdown | ||
this.loadScorecards() | ||
} | ||
this.loadDefaultReviewers() | ||
this.loadWorkflows() | ||
} | ||
|
||
componentDidUpdate (prevProps) { | ||
const { challenge } = this.props | ||
const { challenge, readOnly } = this.props | ||
const prevChallenge = prevProps.challenge | ||
|
||
if (challenge && prevChallenge && | ||
(challenge.type !== prevChallenge.type || challenge.track !== prevChallenge.track)) { | ||
this.loadScorecards() | ||
if (readOnly) { | ||
this.loadSpecificScorecards() | ||
} else { | ||
this.loadScorecards() | ||
} | ||
} | ||
|
||
if (challenge && prevChallenge && | ||
|
@@ -69,6 +79,27 @@ class ChallengeReviewerField extends Component { | |
loadScorecards(filters) | ||
} | ||
|
||
loadSpecificScorecards () { | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { challenge, loadScorecardById } = this.props | ||
const reviewers = challenge.reviewers || [] | ||
|
||
// Get unique scorecard IDs from reviewers | ||
const scorecardIds = [...new Set( | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
reviewers | ||
.filter(reviewer => reviewer.scorecardId) | ||
.map(reviewer => reviewer.scorecardId) | ||
)] | ||
|
||
if (scorecardIds.length === 0) { | ||
return | ||
} | ||
|
||
// Load each scorecard individually | ||
scorecardIds.forEach(scorecardId => { | ||
loadScorecardById(scorecardId) | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
} | ||
|
||
loadDefaultReviewers () { | ||
const { challenge, loadDefaultReviewers } = this.props | ||
|
||
|
@@ -240,7 +271,7 @@ class ChallengeReviewerField extends Component { | |
renderReviewerForm (reviewer, index) { | ||
const { challenge, metadata = {}, readOnly = false } = this.props | ||
const { scorecards = [], workflows = [] } = metadata | ||
const validationErrors = this.validateReviewer(reviewer) | ||
const validationErrors = challenge.submitTriggered ? this.validateReviewer(reviewer) : {} | ||
|
||
return ( | ||
<div key={`reviewer-${index}`} className={styles.reviewerForm}> | ||
|
@@ -337,8 +368,10 @@ class ChallengeReviewerField extends Component { | |
))} | ||
</select> | ||
)} | ||
{validationErrors.aiWorkflowId && ( | ||
<div className={styles.fieldError}>{validationErrors.aiWorkflowId}</div> | ||
{!readOnly && challenge.submitTriggered && validationErrors.aiWorkflowId && ( | ||
<div className={styles.error}> | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{validationErrors.aiWorkflowId} | ||
</div> | ||
)} | ||
</div> | ||
) : ( | ||
|
@@ -347,8 +380,15 @@ class ChallengeReviewerField extends Component { | |
{readOnly ? ( | ||
<span> | ||
{(() => { | ||
const { metadata = {} } = this.props | ||
const specificScorecard = metadata.scorecardById | ||
if (specificScorecard && specificScorecard.id === reviewer.scorecardId) { | ||
return `${specificScorecard.name || 'Unknown'} - ${specificScorecard.type || 'Unknown'} (${specificScorecard.challengeTrack || 'Unknown'}) v${specificScorecard.version || 'Unknown'}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling the case where |
||
} | ||
|
||
// Fallback to searching in the general scorecards array | ||
const scorecard = scorecards.find(s => s.id === reviewer.scorecardId) | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return scorecard ? `${scorecard.name} - ${scorecard.type} (${scorecard.challengeTrack}) v${scorecard.version}` : 'Not selected' | ||
return scorecard ? `${scorecard.name || 'Unknown'} - ${scorecard.type || 'Unknown'} (${scorecard.challengeTrack || 'Unknown'}) v${scorecard.version || 'Unknown'}` : 'Not selected' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling the case where |
||
})()} | ||
</span> | ||
) : ( | ||
|
@@ -359,13 +399,15 @@ class ChallengeReviewerField extends Component { | |
<option value=''>Select Scorecard</option> | ||
{scorecards.map(scorecard => ( | ||
<option key={scorecard.id} value={scorecard.id}> | ||
{scorecard.name} - {scorecard.type} ({scorecard.challengeTrack}) v{scorecard.version} | ||
{scorecard.name || 'Unknown'} - {scorecard.type || 'Unknown'} ({scorecard.challengeTrack || 'Unknown'}) v{scorecard.version || 'Unknown'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using a more descriptive placeholder than 'Unknown' for missing scorecard details to provide clearer context to the user. For example, 'Name not available', 'Type not specified', etc. |
||
</option> | ||
))} | ||
</select> | ||
)} | ||
{validationErrors.scorecardId && ( | ||
<div className={styles.fieldError}>{validationErrors.scorecardId}</div> | ||
{!readOnly && challenge.submitTriggered && validationErrors.scorecardId && ( | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div className={styles.error}> | ||
{validationErrors.scorecardId} | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
|
@@ -409,8 +451,10 @@ class ChallengeReviewerField extends Component { | |
))} | ||
</select> | ||
)} | ||
{validationErrors.phaseId && ( | ||
<div className={styles.fieldError}>{validationErrors.phaseId}</div> | ||
{!readOnly && challenge.submitTriggered && validationErrors.phaseId && ( | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div className={styles.error}> | ||
{validationErrors.phaseId} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
|
@@ -433,8 +477,10 @@ class ChallengeReviewerField extends Component { | |
}} | ||
/> | ||
)} | ||
{validationErrors.memberReviewerCount && ( | ||
<div className={styles.fieldError}>{validationErrors.memberReviewerCount}</div> | ||
{!readOnly && challenge.submitTriggered && validationErrors.memberReviewerCount && ( | ||
<div className={styles.error}> | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{validationErrors.memberReviewerCount} | ||
</div> | ||
)} | ||
</div> | ||
|
||
|
@@ -453,8 +499,10 @@ class ChallengeReviewerField extends Component { | |
}} | ||
/> | ||
)} | ||
{validationErrors.basePayment && ( | ||
<div className={styles.fieldError}>{validationErrors.basePayment}</div> | ||
{!readOnly && challenge.submitTriggered && validationErrors.basePayment && ( | ||
<div className={styles.error}> | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{validationErrors.basePayment} | ||
</div> | ||
)} | ||
</div> | ||
|
||
|
@@ -617,13 +665,15 @@ ChallengeReviewerField.propTypes = { | |
metadata: PropTypes.shape({ | ||
scorecards: PropTypes.array, | ||
defaultReviewers: PropTypes.array, | ||
workflows: PropTypes.array | ||
workflows: PropTypes.array, | ||
scorecardById: PropTypes.object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a default value for |
||
}), | ||
isLoading: PropTypes.bool, | ||
readOnly: PropTypes.bool, | ||
loadScorecards: PropTypes.func.isRequired, | ||
loadDefaultReviewers: PropTypes.func.isRequired, | ||
loadWorkflows: PropTypes.func.isRequired | ||
loadWorkflows: PropTypes.func.isRequired, | ||
loadScorecardById: PropTypes.func.isRequired | ||
} | ||
|
||
const mapStateToProps = (state) => ({ | ||
|
@@ -634,7 +684,8 @@ const mapStateToProps = (state) => ({ | |
const mapDispatchToProps = { | ||
loadScorecards, | ||
loadDefaultReviewers, | ||
loadWorkflows | ||
loadWorkflows, | ||
loadScorecardById | ||
} | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(ChallengeReviewerField) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -769,6 +769,46 @@ class ChallengeEditor extends Component { | |
return true | ||
} | ||
|
||
isValidReviewers () { | ||
const { challenge } = this.state | ||
const reviewers = challenge.reviewers || [] | ||
|
||
if (reviewers.length === 0) { | ||
return true | ||
} | ||
|
||
// Validate each reviewer | ||
for (const reviewer of reviewers) { | ||
const isAI = (reviewer.aiWorkflowId && reviewer.aiWorkflowId.trim() !== '') || !reviewer.isMemberReview | ||
|
||
if (isAI) { | ||
if (!reviewer.aiWorkflowId || reviewer.aiWorkflowId.trim() === '') { | ||
return false | ||
} | ||
} else { | ||
if (!reviewer.scorecardId) { | ||
return false | ||
} | ||
|
||
const memberCount = parseInt(reviewer.memberReviewerCount) || 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
if (memberCount < 1 || !Number.isInteger(memberCount)) { | ||
return false | ||
} | ||
|
||
const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '') | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (basePayment < 0) { | ||
return false | ||
} | ||
} | ||
|
||
if (!reviewer.phaseId) { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
isValidChallenge () { | ||
const { challenge } = this.state | ||
if (this.props.isNew) { | ||
|
@@ -790,6 +830,10 @@ class ChallengeEditor extends Component { | |
return false | ||
} | ||
|
||
if (!this.isValidReviewers()) { | ||
rishabhtc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false | ||
} | ||
|
||
const requiredFields = [ | ||
'trackId', | ||
'typeId', | ||
|
Uh oh!
There was an error while loading. Please reload this page.