From 0b4f6cf53dc2def442c2d663a7f7f4394ca2b069 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Fri, 3 Oct 2025 17:42:41 +0530 Subject: [PATCH 1/3] Validate reviewer errors on saving challenge --- .../ChallengeReviewer-Field.module.scss | 6 +-- .../ChallengeReviewer-Field/index.js | 32 +++++++++----- src/components/ChallengeEditor/index.js | 44 +++++++++++++++++++ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss index 98c5d72d..6ab51757 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss @@ -186,11 +186,7 @@ .error { color: $tc-red; - background-color: #ffebee; - padding: 15px; - border-radius: 4px; - border: 1px solid #ffcdd2; - margin-bottom: 20px; + padding: 5px; } .validationErrors { diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 175fcc3b..40b216d0 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -240,7 +240,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 (
@@ -337,8 +337,10 @@ class ChallengeReviewerField extends Component { ))} )} - {validationErrors.aiWorkflowId && ( -
{validationErrors.aiWorkflowId}
+ {!readOnly && challenge.submitTriggered && validationErrors.aiWorkflowId && ( +
+ {validationErrors.aiWorkflowId} +
)}
) : ( @@ -364,8 +366,10 @@ class ChallengeReviewerField extends Component { ))} )} - {validationErrors.scorecardId && ( -
{validationErrors.scorecardId}
+ {!readOnly && challenge.submitTriggered && validationErrors.scorecardId && ( +
+ {validationErrors.scorecardId} +
)} )} @@ -409,8 +413,10 @@ class ChallengeReviewerField extends Component { ))} )} - {validationErrors.phaseId && ( -
{validationErrors.phaseId}
+ {!readOnly && challenge.submitTriggered && validationErrors.phaseId && ( +
+ {validationErrors.phaseId} +
)} @@ -433,8 +439,10 @@ class ChallengeReviewerField extends Component { }} /> )} - {validationErrors.memberReviewerCount && ( -
{validationErrors.memberReviewerCount}
+ {!readOnly && challenge.submitTriggered && validationErrors.memberReviewerCount && ( +
+ {validationErrors.memberReviewerCount} +
)} @@ -453,8 +461,10 @@ class ChallengeReviewerField extends Component { }} /> )} - {validationErrors.basePayment && ( -
{validationErrors.basePayment}
+ {!readOnly && challenge.submitTriggered && validationErrors.basePayment && ( +
+ {validationErrors.basePayment} +
)} diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index b8d2cd87..3695a754 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -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 === false) + + if (isAI) { + if (!reviewer.aiWorkflowId || reviewer.aiWorkflowId.trim() === '') { + return false + } + } else { + if (!reviewer.scorecardId) { + return false + } + + const memberCount = parseInt(reviewer.memberReviewerCount) || 1 + if (memberCount < 1 || !Number.isInteger(memberCount)) { + return false + } + + const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '') + 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()) { + return false + } + const requiredFields = [ 'trackId', 'typeId', From 261533a0f65a0b852f0049df6d909fafddc0023e Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Fri, 3 Oct 2025 18:47:35 +0530 Subject: [PATCH 2/3] Optmized fetching scorecard on Challenge View page --- src/actions/challenges.js | 29 ++++++++++ .../ChallengeReviewer-Field.module.scss | 7 --- .../ChallengeReviewer-Field/index.js | 58 ++++++++++++++++--- src/services/challenges.js | 9 +++ 4 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 74a7252d..187d1f1e 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -23,6 +23,7 @@ import { updateChallengeSkillsApi, fetchDefaultReviewers, fetchScorecards, + fetchScorecardById, fetchWorkflows } from '../services/challenges' import { searchProfilesByUserIds } from '../services/user' @@ -794,6 +795,34 @@ export function loadScorecards (filters = {}) { } } +/** + * Load a specific scorecard by ID + * @param {string} scorecardId the scorecard ID + */ +export function loadScorecardById (scorecardId) { + return async (dispatch) => { + try { + const scorecard = await fetchScorecardById(scorecardId) + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'scorecardById', + metadataValue: { + ...scorecard, + id: scorecardId + } + }) + } catch (error) { + console.error('Error loading scorecard by ID:', error) + // Return null on error + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'scorecardById', + metadataValue: null + }) + } + } +} + /** * Load default reviewers * @param {Object} filters filters for default reviewers diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss index 6ab51757..3dfcc85e 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss @@ -207,13 +207,6 @@ margin-bottom: 0; } -.fieldError { - color: #DC3545; - font-size: 12px; - margin-top: 4px; - display: block; -} - // Responsive adjustments @media (max-width: 768px) { .formRow { diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 40b216d0..bdb913d9 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -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 () { + const { challenge, loadScorecardById } = this.props + const reviewers = challenge.reviewers || [] + + // Get unique scorecard IDs from reviewers + const scorecardIds = [...new Set( + reviewers + .filter(reviewer => reviewer.scorecardId) + .map(reviewer => reviewer.scorecardId) + )] + + if (scorecardIds.length === 0) { + return + } + + // Load each scorecard individually + scorecardIds.forEach(scorecardId => { + loadScorecardById(scorecardId) + }) + } + loadDefaultReviewers () { const { challenge, loadDefaultReviewers } = this.props @@ -349,6 +380,16 @@ class ChallengeReviewerField extends Component { {readOnly ? ( {(() => { + // In read-only mode, try to get the specific scorecard first + if (this.props.readOnly) { + const { metadata = {} } = this.props + const specificScorecard = metadata.scorecardById + if (specificScorecard && specificScorecard.id === reviewer.scorecardId) { + return `${specificScorecard.name} - ${specificScorecard.type} (${specificScorecard.challengeTrack}) v${specificScorecard.version}` + } + } + + // Fallback to searching in the general scorecards array const scorecard = scorecards.find(s => s.id === reviewer.scorecardId) return scorecard ? `${scorecard.name} - ${scorecard.type} (${scorecard.challengeTrack}) v${scorecard.version}` : 'Not selected' })()} @@ -627,13 +668,15 @@ ChallengeReviewerField.propTypes = { metadata: PropTypes.shape({ scorecards: PropTypes.array, defaultReviewers: PropTypes.array, - workflows: PropTypes.array + workflows: PropTypes.array, + scorecardById: PropTypes.object }), 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) => ({ @@ -644,7 +687,8 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = { loadScorecards, loadDefaultReviewers, - loadWorkflows + loadWorkflows, + loadScorecardById } export default connect(mapStateToProps, mapDispatchToProps)(ChallengeReviewerField) diff --git a/src/services/challenges.js b/src/services/challenges.js index 5363dfee..6a019f5e 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -335,6 +335,15 @@ export async function fetchScorecards (filters = {}) { const response = await axiosInstance.get(`${SCORECARDS_API_URL}?${qs.stringify(query, { encode: false })}`) return _.get(response, 'data', {}) } +/** + * Api request for fetching a specific scorecard by ID + * @param {string} scorecardId the scorecard ID + * @returns {Promise<*>} + */ +export async function fetchScorecardById (scorecardId) { + const response = await axiosInstance.get(`${SCORECARDS_API_URL}/${scorecardId}`) + return _.get(response, 'data', {}) +} /** * Api request for fetching default reviewers * @param {Object} filters filters for default reviewers From 2eccf36057e55e16cbc974c072ac9532e648c5a6 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Fri, 3 Oct 2025 19:08:48 +0530 Subject: [PATCH 3/3] minor code cleanups --- .../ChallengeReviewer-Field/index.js | 15 ++++++--------- src/components/ChallengeEditor/index.js | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index bdb913d9..7befe679 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -380,18 +380,15 @@ class ChallengeReviewerField extends Component { {readOnly ? ( {(() => { - // In read-only mode, try to get the specific scorecard first - if (this.props.readOnly) { - const { metadata = {} } = this.props - const specificScorecard = metadata.scorecardById - if (specificScorecard && specificScorecard.id === reviewer.scorecardId) { - return `${specificScorecard.name} - ${specificScorecard.type} (${specificScorecard.challengeTrack}) v${specificScorecard.version}` - } + 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'}` } // Fallback to searching in the general scorecards array const scorecard = scorecards.find(s => s.id === reviewer.scorecardId) - 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' })()} ) : ( @@ -402,7 +399,7 @@ class ChallengeReviewerField extends Component { {scorecards.map(scorecard => ( ))} diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 3695a754..913cc829 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -779,7 +779,7 @@ class ChallengeEditor extends Component { // Validate each reviewer for (const reviewer of reviewers) { - const isAI = (reviewer.aiWorkflowId && reviewer.aiWorkflowId.trim() !== '') || (reviewer.isMemberReview === false) + const isAI = (reviewer.aiWorkflowId && reviewer.aiWorkflowId.trim() !== '') || !reviewer.isMemberReview if (isAI) { if (!reviewer.aiWorkflowId || reviewer.aiWorkflowId.trim() === '') {