From abdc1799302193e507fb46a3d364c37924d140d7 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 4 Sep 2025 21:42:34 +0530 Subject: [PATCH 01/13] Added reviewer section --- .../ChallengeReviewer-Field.module.scss | 234 +++++++++ .../ChallengeReviewer-Field/index.js | 451 ++++++++++++++++++ .../ChallengeEditor/TextEditor-Field/index.js | 15 +- src/components/ChallengeEditor/index.js | 8 +- src/config/constants.js | 7 + 5 files changed, 712 insertions(+), 3 deletions(-) create mode 100644 src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss create mode 100644 src/components/ChallengeEditor/ChallengeReviewer-Field/index.js diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss new file mode 100644 index 00000000..af7aa6cb --- /dev/null +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss @@ -0,0 +1,234 @@ +@import '../../../styles/includes'; + +.error { + color: $tc-red; +} + +.row { + box-sizing: border-box; + display: flex; + flex-direction: row; + margin: 30px 30px 0 30px; + align-content: space-between; + justify-content: flex-start; + + .field { + @include upto-sm { + display: block; + padding-bottom: 10px; + } + + label { + @include roboto-bold(); + font-size: 16px; + line-height: 19px; + font-weight: 500; + color: $tc-gray-80; + } + + &.col1 { + max-width: 185px; + min-width: 185px; + margin-right: 14px; + white-space: nowrap; + display: flex; + align-items: center; + } + + &.col2 { + align-self: flex-end; + margin-bottom: auto; + margin-top: auto; + display: flex; + flex-direction: column; + width: 600px; + } + } +} + +.description { + color: #666; + margin-bottom: 20px; + font-size: 14px; + line-height: 1.4; +} + +.noReviewers { + text-align: center; + padding: 30px; + color: #999; + font-style: italic; + background-color: #f5f5f5; + border-radius: 4px; + margin-bottom: 20px; +} + +.defaultReviewerNote { + margin-top: 20px; + padding: 15px; + background-color: #e3f2fd; + border: 1px solid #bbdefb; + border-radius: 4px; +} + +.defaultReviewerNote p { + margin: 0 0 15px 0; + color: #1976d2; + font-style: normal; +} + +.reviewerForm { + background-color: white; + border: 1px solid #ddd; + border-radius: 4px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.reviewerHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.reviewerHeader h4 { + margin: 0; + color: #333; + font-size: 16px; + font-weight: 600; +} + +.formRow { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 15px; +} + +.formGroup { + flex: 1; + min-width: 200px; +} + +.formGroup label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #555; + font-size: 14px; +} + +.formGroup input, +.formGroup select { + width: 100%; + padding: 8px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + background-color: white; +} + +.formGroup input:focus, +.formGroup select:focus { + outline: none; + border-color: #0066cc; + box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); +} + +.addButton { + text-align: center; + margin-top: 20px; +} + +.summary { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 20px; + margin: 20px 0; +} + +.summary h4 { + margin: 0 0 15px 0; + color: #333; + font-size: 16px; + font-weight: 600; +} + +.summaryRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #eee; +} + +.summaryRow:last-child { + border-bottom: none; + font-weight: 600; + color: #0066cc; +} + +.summaryRow span:first-child { + color: #666; +} + +.summaryRow span:last-child { + font-weight: 500; +} + +.loading { + text-align: center; + padding: 40px; + color: #666; + font-style: italic; +} + +.error { + color: #d32f2f; + background-color: #ffebee; + padding: 15px; + border-radius: 4px; + border: 1px solid #ffcdd2; + margin-bottom: 20px; +} + +.validationErrors { + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; + padding: 10px; + margin-bottom: 15px; +} + +.validationError { + color: #856404; + font-size: 13px; + margin-bottom: 5px; +} + +.validationError:last-child { + margin-bottom: 0; +} + +// Responsive adjustments +@media (max-width: 768px) { + .formRow { + flex-direction: column; + gap: 15px; + } + + .formGroup { + min-width: 100%; + } + + .reviewerHeader { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } +} diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js new file mode 100644 index 00000000..04db6b20 --- /dev/null +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -0,0 +1,451 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { axiosInstance as axios } from '../../../services/axiosWithAuth' +import cn from 'classnames' +import { PrimaryButton, OutlineButton } from '../../Buttons' +import { REVIEW_OPPORTUNITY_TYPES } from '../../../config/constants' +import styles from './ChallengeReviewer-Field.module.scss' + +class ChallengeReviewerField extends Component { + constructor (props) { + super(props) + this.state = { + scorecards: [], + defaultReviewers: [], + isLoadingScorecards: false, + isLoadingDefaults: false, + error: null + } + + // Bind methods + this.addReviewer = this.addReviewer.bind(this) + this.removeReviewer = this.removeReviewer.bind(this) + this.updateReviewer = this.updateReviewer.bind(this) + this.renderReviewerForm = this.renderReviewerForm.bind(this) + this.handleApplyDefault = this.handleApplyDefault.bind(this) + } + + componentDidMount () { + this.loadScorecards() + this.loadDefaultReviewers() + } + + async loadScorecards () { + this.setState({ isLoadingScorecards: true, error: null }) + try { + const { challenge } = this.props + // Build query parameters for the scorecard API + const params = { + perPage: 100, // Get more scorecards to ensure we have enough + page: 1 + } + + // Add challenge track if available + if (challenge.trackId) { + // Map track ID to track name for the API + const trackMapping = { + '1': 'DEVELOPMENT', + '2': 'DESIGN', + '3': 'DATA_SCIENCE', + '4': 'QA' + } + const trackName = trackMapping[challenge.trackId] || 'DEVELOPMENT' + params.challengeTrack = trackName + } + + // Fetch scorecards from review-api-v6 + const response = await axios.get('https://api.topcoder-dev.com/v6/scorecards', { + params + }) + + if (response.status === 200) { + const data = response.data + const scorecards = data.scoreCards || [] + this.setState({ scorecards }) + } else { + throw new Error('Failed to load scorecards') + } + } catch (error) { + console.error('Error loading scorecards:', error) + // Use mock data for development/testing + // const mockScorecards = [ + // { id: 'scorecard-1', name: 'Standard Development Review' }, + // { id: 'scorecard-2', name: 'Design Review' }, + // { id: 'scorecard-3', name: 'Data Science Review' }, + // { id: 'scorecard-4', name: 'QA Review' } + // ] + this.setState({ scorecards: [] }) + } finally { + this.setState({ isLoadingScorecards: false }) + } + } + + async loadDefaultReviewers () { + this.setState({ isLoadingDefaults: true, error: null }) + try { + const response = await axios.get('https://api.topcoder-dev.com/v6/challenge/default-reviewers') + if (response.status === 200) { + const data = response.data + this.setState({ defaultReviewers: data }) + } else { + throw new Error('Failed to load default reviewers') + } + } catch (error) { + console.error('Error loading default reviewers:', error) + // Use mock data for development/testing + const mockDefaultReviewers = [ + { + id: 'default-1', + typeId: '1', + trackId: '1', + scorecardId: 'scorecard-1', + isMemberReview: true, + memberReviewerCount: 2, + phaseId: 'aa5a3f78-79e0-4bf7-93ff-b11e8f5b398b', + basePayment: 100, + incrementalPayment: 50, + opportunityType: 'REGULAR_REVIEW', + isAIReviewer: false + }, + { + id: 'default-2', + typeId: '2', + trackId: '1', + scorecardId: 'scorecard-1', + isMemberReview: true, + memberReviewerCount: 1, + phaseId: '003a4b14-de5d-43fc-9e35-835dbeb6af1f', + basePayment: 75, + incrementalPayment: 25, + opportunityType: 'REGULAR_REVIEW', + isAIReviewer: false + } + ] + this.setState({ defaultReviewers: mockDefaultReviewers }) + } finally { + this.setState({ isLoadingDefaults: false }) + } + } + + addReviewer () { + const { challenge, onUpdateOthers } = this.props + const currentReviewers = challenge.reviewers || [] + + // Create a new default reviewer based on track and type + const defaultReviewer = this.findDefaultReviewer() + + // Get the first available phase if phases exist + const firstPhase = challenge.phases && challenge.phases.length > 0 ? challenge.phases[0] : null + + const newReviewer = { + id: 'temp-' + Date.now(), + scorecardId: (defaultReviewer && defaultReviewer.scorecardId) || '', + isMemberReview: true, + memberReviewerCount: (defaultReviewer && defaultReviewer.memberReviewerCount) || 1, + phaseId: (defaultReviewer && defaultReviewer.phaseId) || (firstPhase ? firstPhase.id : ''), + basePayment: (defaultReviewer && defaultReviewer.basePayment) || 0, + incrementalPayment: (defaultReviewer && defaultReviewer.incrementalPayment) || 0, + type: (defaultReviewer && defaultReviewer.opportunityType) || REVIEW_OPPORTUNITY_TYPES.REGULAR_REVIEW, + isAIReviewer: false + } + + const updatedReviewers = currentReviewers.concat([newReviewer]) + onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + } + + removeReviewer (index) { + const { challenge, onUpdateOthers } = this.props + const currentReviewers = challenge.reviewers || [] + const updatedReviewers = currentReviewers.filter((_, i) => i !== index) + onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + } + + updateReviewer (index, field, value) { + const { challenge, onUpdateOthers } = this.props + const currentReviewers = challenge.reviewers || [] + const updatedReviewers = currentReviewers.slice() + const fieldUpdate = {} + fieldUpdate[field] = value + updatedReviewers[index] = Object.assign({}, updatedReviewers[index], fieldUpdate) + onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + } + + findDefaultReviewer () { + const { challenge } = this.props + const { defaultReviewers } = this.state + + if (!challenge || !challenge.trackId || !challenge.typeId) { + return null + } + + return defaultReviewers.find(dr => + dr.trackId === challenge.trackId && dr.typeId === challenge.typeId + ) + } + + validateReviewer (reviewer) { + const errors = [] + + if (!reviewer.scorecardId) { + errors.push('Scorecard is required') + } + + if (!reviewer.phaseId) { + errors.push('Phase is required') + } + + if (!reviewer.isAIReviewer && (!reviewer.memberReviewerCount || reviewer.memberReviewerCount < 1)) { + errors.push('Number of reviewers must be at least 1') + } + + if (!reviewer.isAIReviewer && (!reviewer.basePayment || reviewer.basePayment < 0)) { + errors.push('Base payment must be non-negative') + } + + return errors + } + + handleApplyDefault () { + const defaultReviewer = this.findDefaultReviewer() + if (defaultReviewer) { + this.addReviewer() + } + } + + renderReviewerForm (reviewer, index) { + const { challenge } = this.props + const { scorecards } = this.state + const validationErrors = this.validateReviewer(reviewer) + + return ( +
+
+

Reviewer {index + 1}

+ this.removeReviewer(index)} + /> +
+ + {validationErrors.length > 0 && ( +
+ {validationErrors.map((error, i) => ( +
{error}
+ ))} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {!reviewer.isAIReviewer && ( +
+
+ + this.updateReviewer(index, 'memberReviewerCount', parseInt(e.target.value))} + /> +
+ +
+ + this.updateReviewer(index, 'basePayment', parseFloat(e.target.value))} + /> +
+ +
+ + this.updateReviewer(index, 'incrementalPayment', parseFloat(e.target.value))} + /> +
+
+ )} + +
+
+ + +
+
+
+ ) + } + + render () { + const { challenge } = this.props + const { isLoadingScorecards, isLoadingDefaults, error } = this.state + const reviewers = challenge.reviewers || [] + + if (isLoadingScorecards || isLoadingDefaults) { + return ( +
+
+ +
+
+
Loading...
+
+
+ ) + } + + // Only show error if there's a real error, not just missing data + if (error && !this.state.scorecards.length && !this.state.defaultReviewers.length) { + return ( +
+
+ +
+
+ {error} +
+
+ ) + } + + return ( + <> +
+
+ +
+
+
+ Configure how this challenge will be reviewed. You can add multiple reviewers including AI and member reviewers. +
+ + {reviewers.length === 0 && ( +
+

No reviewers configured. Click "Add Reviewer" to get started.

+ {this.findDefaultReviewer() && ( +
+

Note: Default reviewer configuration is available for this track and type combination.

+ +
+ )} +
+ )} + + {reviewers.map((reviewer, index) => + this.renderReviewerForm(reviewer, index) + )} + + {reviewers.length > 0 && ( +
+

Review Summary

+
+ Total Member Reviewers: + {reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => sum + (r.memberReviewerCount || 0), 0)} +
+
+ Total AI Reviewers: + {reviewers.filter(r => r.isAIReviewer).length} +
+
+ Total Review Cost: + ${reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => { + const base = r.basePayment || 0 + const incremental = r.incrementalPayment || 0 + const count = r.memberReviewerCount || 1 + return sum + base + (incremental * (count - 1)) + }, 0).toFixed(2)} +
+
+ )} + +
+ +
+
+
+ + ) + } +} + +ChallengeReviewerField.propTypes = { + challenge: PropTypes.object.isRequired, + onUpdateOthers: PropTypes.func.isRequired +} + +export default ChallengeReviewerField diff --git a/src/components/ChallengeEditor/TextEditor-Field/index.js b/src/components/ChallengeEditor/TextEditor-Field/index.js index 6a90a139..fc45eeef 100644 --- a/src/components/ChallengeEditor/TextEditor-Field/index.js +++ b/src/components/ChallengeEditor/TextEditor-Field/index.js @@ -11,6 +11,7 @@ import styles from './TextEditor-Field.module.scss' import PropTypes from 'prop-types' import DescriptionField from '../Description-Field' import { PrimaryButton } from '../../Buttons' +import ChallengeReviewerField from '../ChallengeReviewer-Field' class TextEditorField extends Component { constructor (props) { @@ -78,6 +79,12 @@ class TextEditorField extends Component { /> )} + {this.props.showReviewerField && ( + + )} {}, onUpdateSkills: () => {}, onUpdateMultiSelect: () => {}, - readOnly: false + readOnly: false, + showReviewerField: false, + onUpdateOthers: () => {} } TextEditorField.propTypes = { @@ -145,7 +154,9 @@ TextEditorField.propTypes = { onUpdateSkills: PropTypes.func, onUpdateMultiSelect: PropTypes.func, shouldShowPrivateDescription: PropTypes.bool, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + showReviewerField: PropTypes.bool, + onUpdateOthers: PropTypes.func } export default TextEditorField diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 50111604..a3c21e9b 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -239,6 +239,9 @@ class ChallengeEditor extends Component { const challengeDetail = { ...challengeData } const isRequiredNda = challengeDetail.terms && _.some(challengeDetail.terms, { id: DEFAULT_NDA_UUID }) const isOpenAdvanceSettings = challengeDetail.groups.length > 0 || isRequiredNda + if (!challengeDetail.reviewer) { + challengeDetail.reviewer = [] + } setState({ challenge: challengeDetail, assignedMemberDetails, @@ -1038,7 +1041,8 @@ class ChallengeEditor extends Component { terms: [{ id: DEFAULT_TERM_UUID, roleId: SUBMITTER_ROLE_UUID }], groups: [], milestoneId, - tags + tags, + reviewer: [] // prizeSets: this.getDefaultPrizeSets() } if (isTask) { @@ -1854,6 +1858,8 @@ class ChallengeEditor extends Component { onUpdateSkills={this.onUpdateSkills} onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} + showReviewerField + onUpdateOthers={this.onUpdateOthers} /> {/* hide until challenge API change is pushed to PROD https://github.com/topcoder-platform/challenge-api/issues/348 */} {false && Date: Mon, 8 Sep 2025 18:40:11 +0530 Subject: [PATCH 02/13] Passed reviewer details --- .../ChallengeReviewer-Field/index.js | 15 ++++++++++++++- src/components/ChallengeEditor/index.js | 3 ++- src/config/constants.js | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 04db6b20..7a1901fb 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -83,7 +83,20 @@ class ChallengeReviewerField extends Component { async loadDefaultReviewers () { this.setState({ isLoadingDefaults: true, error: null }) try { - const response = await axios.get('https://api.topcoder-dev.com/v6/challenge/default-reviewers') + const { challenge } = this.props + + // only load default reviewers if we have typeId and trackId + if (!challenge.typeId || !challenge.trackId) { + console.log('Cannot load default reviewers: missing typeId or trackId') + this.setState({ defaultReviewer: [] }) + return + } + const response = await axios.get('https://api.topcoder-dev.com/v6/challenges/default-reviewers', { + params: { + typeId: challenge.typeId, + trackId: challenge.trackId + } + }) if (response.status === 200) { const data = response.data this.setState({ defaultReviewers: data }) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index a3c21e9b..5c26b56f 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -946,7 +946,8 @@ class ChallengeEditor extends Component { 'milestoneId', 'discussions', 'task', - 'skills' + 'skills', + 'reviewers' ], this.state.challenge) const isTask = _.find(metadata.challengeTypes, { id: challenge.typeId, isTask: true }) challenge.legacy = _.assign(this.state.challenge.legacy, { diff --git a/src/config/constants.js b/src/config/constants.js index 16e7036c..8955269c 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -239,7 +239,7 @@ export const REVIEW_TYPES = { } export const REVIEW_OPPORTUNITY_TYPES = { - CODE_REVIEW: 'CODE_REVIEW', + REGULAR_REVIEW: 'REGULAR_REVIEW', SPEC_REVIEW: 'SPEC_REVIEW', ITERATIVE_REVIEW: 'ITERATIVE_REVIEW', SCENARIOS_REVIEW: 'SCENARIOS_REVIEW' From 5f3750735a848c451a3ff4cd94d4c2f3acb2b4b8 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Tue, 9 Sep 2025 20:17:53 +0530 Subject: [PATCH 03/13] Restructured the code: Moved the logic to services, actions and corrected payment logic --- config/constants/development.js | 1 + config/constants/production.js | 1 + src/actions/challenges.js | 60 ++++- .../ChallengeReviewer-Field.module.scss | 6 +- .../ChallengeReviewer-Field/index.js | 212 +++++++----------- .../ChallengeEditor/TextEditor-Field/index.js | 7 +- src/components/ChallengeEditor/index.js | 2 +- src/containers/ChallengeEditor/index.js | 14 +- src/services/challenges.js | 26 +++ 9 files changed, 182 insertions(+), 147 deletions(-) diff --git a/config/constants/development.js b/config/constants/development.js index 773fd132..438ffea1 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -26,6 +26,7 @@ module.exports = { RESOURCE_ROLES_API_URL: `${DEV_API_HOSTNAME}/v6/resource-roles`, SUBMISSIONS_API_URL: `${DEV_API_HOSTNAME}/v6/submissions`, REVIEW_TYPE_API_URL: `${DEV_API_HOSTNAME}/v6/reviewTypes`, + SCORECARDS_API_URL: `${DEV_API_HOSTNAME}/v6/scorecards`, SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`, STUDIO_URL: `https://studio.${DOMAIN}`, CONNECT_APP_URL: `https://connect.${DOMAIN}`, diff --git a/config/constants/production.js b/config/constants/production.js index 882ee5be..cb25c033 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -27,6 +27,7 @@ module.exports = { RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v5/resource-roles`, SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v5/submissions`, REVIEW_TYPE_API_URL: `${PROD_API_HOSTNAME}/v5/reviewTypes`, + SCORECARDS_API_URL: `${DEV_API_HOSTNAME}/v5/scorecards`, //update to use v6 SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`, STUDIO_URL: `https://studio.${DOMAIN}`, CONNECT_APP_URL: `https://connect.${DOMAIN}`, diff --git a/src/actions/challenges.js b/src/actions/challenges.js index fbdcebbc..11e52aef 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -20,7 +20,9 @@ import { createChallenge as createChallengeAPI, createResource as createResourceAPI, deleteResource as deleteResourceAPI, - updateChallengeSkillsApi + updateChallengeSkillsApi, + fetchDefaultReviewers, + fetchScorecards } from '../services/challenges' import { searchProfilesByUserIds } from '../services/user' import { @@ -170,11 +172,11 @@ export function loadChallengesByPage ( } if (status !== 'all') { - filters['status'] = !status ? undefined : status + filters['status'] = !status ? undefined : _.startCase(status.toLowerCase()) } if (!dashboard && !filters['status'] && !(_.isInteger(projectId) && projectId > 0)) { - filters['status'] = 'ACTIVE' + filters['status'] = 'Active' } if (selfService) { filters.selfService = true @@ -238,7 +240,7 @@ export function loadChallenges ( if (!_.isEmpty(status)) { filters['status'] = status === '' ? undefined : _.startCase(status.toLowerCase()) } else if (!(_.isInteger(projectId) && projectId > 0)) { - filters['status'] = 'ACTIVE' + filters['status'] = 'Active' } let fetchedChallenges = [] @@ -765,3 +767,53 @@ export function updateChallengeSkills (challengeId, skills) { } } } + +/** + * Load scorecards + * @param {Object} filters filters for scorecards + */ +export function loadScorecards (filters = {}) { + return async (dispatch) => { + try { + const scorecards = await fetchScorecards(filters) + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'scorecards', + metadataValue: scorecards.scoreCards || [] + }) + } catch (error) { + console.error('Error loading scorecards:', error) + // Return empty array on error to maintain consistency + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'scorecards', + metadataValue: [] + }) + } + } +} + +/** + * Load default reviewers + * @param {Object} filters filters for default reviewers + */ +export function loadDefaultReviewers (filters = {}) { + return async (dispatch) => { + try { + const defaultReviewers = await fetchDefaultReviewers(filters) + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'defaultReviewers', + metadataValue: defaultReviewers + }) + } catch (error) { + console.error('Error loading default reviewers:', error) + // Return empty array on error to maintain consistency + dispatch({ + type: LOAD_CHALLENGE_METADATA_SUCCESS, + metadataKey: 'defaultReviewers', + metadataValue: [] + }) + } + } +} diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss index af7aa6cb..33b54f77 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss @@ -1,9 +1,5 @@ @import '../../../styles/includes'; -.error { - color: $tc-red; -} - .row { box-sizing: border-box; display: flex; @@ -189,7 +185,7 @@ } .error { - color: #d32f2f; + color: $tc-red; background-color: #ffebee; padding: 15px; border-radius: 4px; diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 7a1901fb..88853f65 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -1,19 +1,16 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import { axiosInstance as axios } from '../../../services/axiosWithAuth' +import { connect } from 'react-redux' import cn from 'classnames' import { PrimaryButton, OutlineButton } from '../../Buttons' import { REVIEW_OPPORTUNITY_TYPES } from '../../../config/constants' +import { loadScorecards, loadDefaultReviewers } from '../../../actions/challenges' import styles from './ChallengeReviewer-Field.module.scss' class ChallengeReviewerField extends Component { constructor (props) { super(props) this.state = { - scorecards: [], - defaultReviewers: [], - isLoadingScorecards: false, - isLoadingDefaults: false, error: null } @@ -27,121 +24,39 @@ class ChallengeReviewerField extends Component { componentDidMount () { this.loadScorecards() - this.loadDefaultReviewers() + this.isLoadingDefaultReviewers() } - async loadScorecards () { - this.setState({ isLoadingScorecards: true, error: null }) - try { - const { challenge } = this.props - // Build query parameters for the scorecard API - const params = { - perPage: 100, // Get more scorecards to ensure we have enough - page: 1 - } - - // Add challenge track if available - if (challenge.trackId) { - // Map track ID to track name for the API - const trackMapping = { - '1': 'DEVELOPMENT', - '2': 'DESIGN', - '3': 'DATA_SCIENCE', - '4': 'QA' - } - const trackName = trackMapping[challenge.trackId] || 'DEVELOPMENT' - params.challengeTrack = trackName - } - - // Fetch scorecards from review-api-v6 - const response = await axios.get('https://api.topcoder-dev.com/v6/scorecards', { - params - }) - - if (response.status === 200) { - const data = response.data - const scorecards = data.scoreCards || [] - this.setState({ scorecards }) - } else { - throw new Error('Failed to load scorecards') - } - } catch (error) { - console.error('Error loading scorecards:', error) - // Use mock data for development/testing - // const mockScorecards = [ - // { id: 'scorecard-1', name: 'Standard Development Review' }, - // { id: 'scorecard-2', name: 'Design Review' }, - // { id: 'scorecard-3', name: 'Data Science Review' }, - // { id: 'scorecard-4', name: 'QA Review' } - // ] - this.setState({ scorecards: [] }) - } finally { - this.setState({ isLoadingScorecards: false }) + loadScorecards () { + const { challenge, loadScorecards } = this.props + // Build query parameters for the scorecard API + const filters = {} + + // Add challenge track if available + if (challenge.track) { + filters.challengeTrack = challenge.track.toUpperCase() } + + loadScorecards(filters) } - async loadDefaultReviewers () { - this.setState({ isLoadingDefaults: true, error: null }) - try { - const { challenge } = this.props - - // only load default reviewers if we have typeId and trackId - if (!challenge.typeId || !challenge.trackId) { - console.log('Cannot load default reviewers: missing typeId or trackId') - this.setState({ defaultReviewer: [] }) - return - } - const response = await axios.get('https://api.topcoder-dev.com/v6/challenges/default-reviewers', { - params: { - typeId: challenge.typeId, - trackId: challenge.trackId - } - }) - if (response.status === 200) { - const data = response.data - this.setState({ defaultReviewers: data }) - } else { - throw new Error('Failed to load default reviewers') - } - } catch (error) { - console.error('Error loading default reviewers:', error) - // Use mock data for development/testing - const mockDefaultReviewers = [ - { - id: 'default-1', - typeId: '1', - trackId: '1', - scorecardId: 'scorecard-1', - isMemberReview: true, - memberReviewerCount: 2, - phaseId: 'aa5a3f78-79e0-4bf7-93ff-b11e8f5b398b', - basePayment: 100, - incrementalPayment: 50, - opportunityType: 'REGULAR_REVIEW', - isAIReviewer: false - }, - { - id: 'default-2', - typeId: '2', - trackId: '1', - scorecardId: 'scorecard-1', - isMemberReview: true, - memberReviewerCount: 1, - phaseId: '003a4b14-de5d-43fc-9e35-835dbeb6af1f', - basePayment: 75, - incrementalPayment: 25, - opportunityType: 'REGULAR_REVIEW', - isAIReviewer: false - } - ] - this.setState({ defaultReviewers: mockDefaultReviewers }) - } finally { - this.setState({ isLoadingDefaults: false }) + isLoadingDefaultReviewers () { + const { challenge, loadDefaultReviewers } = this.props + + // only load default reviewers if we have typeId and trackId + if (!challenge.typeId || !challenge.trackId) { + console.log('Cannot load default reviewers: missing typeId or trackId') + return } + + loadDefaultReviewers({ + typeId: challenge.typeId, + trackId: challenge.trackId + }) } addReviewer () { - const { challenge, onUpdateOthers } = this.props + const { challenge, onUpdateReviewers } = this.props const currentReviewers = challenge.reviewers || [] // Create a new default reviewer based on track and type @@ -151,7 +66,6 @@ class ChallengeReviewerField extends Component { const firstPhase = challenge.phases && challenge.phases.length > 0 ? challenge.phases[0] : null const newReviewer = { - id: 'temp-' + Date.now(), scorecardId: (defaultReviewer && defaultReviewer.scorecardId) || '', isMemberReview: true, memberReviewerCount: (defaultReviewer && defaultReviewer.memberReviewerCount) || 1, @@ -163,29 +77,29 @@ class ChallengeReviewerField extends Component { } const updatedReviewers = currentReviewers.concat([newReviewer]) - onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) } removeReviewer (index) { - const { challenge, onUpdateOthers } = this.props + const { challenge, onUpdateReviewers } = this.props const currentReviewers = challenge.reviewers || [] const updatedReviewers = currentReviewers.filter((_, i) => i !== index) - onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) } updateReviewer (index, field, value) { - const { challenge, onUpdateOthers } = this.props + const { challenge, onUpdateReviewers } = this.props const currentReviewers = challenge.reviewers || [] const updatedReviewers = currentReviewers.slice() const fieldUpdate = {} fieldUpdate[field] = value updatedReviewers[index] = Object.assign({}, updatedReviewers[index], fieldUpdate) - onUpdateOthers({ field: 'reviewers', value: updatedReviewers }) + onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) } findDefaultReviewer () { - const { challenge } = this.props - const { defaultReviewers } = this.state + const { challenge, metadata } = this.props + const { defaultReviewers = [] } = metadata if (!challenge || !challenge.trackId || !challenge.typeId) { return null @@ -226,12 +140,12 @@ class ChallengeReviewerField extends Component { } renderReviewerForm (reviewer, index) { - const { challenge } = this.props - const { scorecards } = this.state + const { challenge, metadata } = this.props + const { scorecards = [] } = metadata const validationErrors = this.validateReviewer(reviewer) return ( -
+

Reviewer {index + 1}

{ const isAI = e.target.value === 'ai' - this.updateReviewer(index, 'isAIReviewer', isAI) - this.updateReviewer(index, 'isMemberReview', !isAI) + const { challenge, onUpdateReviewers } = this.props + const currentReviewers = challenge.reviewers || [] + const updatedReviewers = currentReviewers.slice() + + // Update both fields atomically to ensure XOR constraint is satisfied + // Maintain correct field order as expected by API schema + const currentReviewer = updatedReviewers[index] + updatedReviewers[index] = { + scorecardId: currentReviewer.scorecardId, + isMemberReview: !isAI, + memberReviewerCount: currentReviewer.memberReviewerCount, + phaseId: currentReviewer.phaseId, + basePayment: currentReviewer.basePayment, + incrementalPayment: currentReviewer.incrementalPayment, + type: currentReviewer.type, + isAIReviewer: isAI + } + + onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) }} > @@ -357,11 +288,12 @@ class ChallengeReviewerField extends Component { } render () { - const { challenge } = this.props - const { isLoadingScorecards, isLoadingDefaults, error } = this.state + const { challenge, metadata, isLoading } = this.props + const { error } = this.state + const { scorecards = [], defaultReviewers = [] } = metadata const reviewers = challenge.reviewers || [] - if (isLoadingScorecards || isLoadingDefaults) { + if (isLoading) { return (
@@ -375,7 +307,7 @@ class ChallengeReviewerField extends Component { } // Only show error if there's a real error, not just missing data - if (error && !this.state.scorecards.length && !this.state.defaultReviewers.length) { + if (error && !scorecards.length && !defaultReviewers.length) { return (
@@ -434,9 +366,8 @@ class ChallengeReviewerField extends Component { Total Review Cost: ${reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => { const base = r.basePayment || 0 - const incremental = r.incrementalPayment || 0 const count = r.memberReviewerCount || 1 - return sum + base + (incremental * (count - 1)) + return sum + (base * count) }, 0).toFixed(2)}
@@ -458,7 +389,24 @@ class ChallengeReviewerField extends Component { ChallengeReviewerField.propTypes = { challenge: PropTypes.object.isRequired, - onUpdateOthers: PropTypes.func.isRequired + onUpdateReviewers: PropTypes.func.isRequired, + metadata: PropTypes.shape({ + scorecards: PropTypes.array, + defaultReviewers: PropTypes.array + }), + isLoading: PropTypes.bool, + loadScorecards: PropTypes.func.isRequired, + loadDefaultReviewers: PropTypes.func.isRequired +} + +const mapStateToProps = (state) => ({ + metadata: state.challenges.metadata, + isLoading: state.challenges.isLoading +}) + +const mapDispatchToProps = { + loadScorecards, + loadDefaultReviewers } -export default ChallengeReviewerField +export default connect(mapStateToProps, mapDispatchToProps)(ChallengeReviewerField) diff --git a/src/components/ChallengeEditor/TextEditor-Field/index.js b/src/components/ChallengeEditor/TextEditor-Field/index.js index fc45eeef..b8e992d0 100644 --- a/src/components/ChallengeEditor/TextEditor-Field/index.js +++ b/src/components/ChallengeEditor/TextEditor-Field/index.js @@ -37,7 +37,8 @@ class TextEditorField extends Component { onUpdateMultiSelect, shouldShowPrivateDescription, onUpdateMetadata, - readOnly + readOnly, + showReviewerField } = this.props const { addedNewPrivateDescription } = this.state const showShowPrivateDescriptionField = addedNewPrivateDescription || (challenge.privateDescription !== null && challenge.privateDescription !== undefined) @@ -79,10 +80,10 @@ class TextEditorField extends Component { />
)} - {this.props.showReviewerField && ( + {showReviewerField && ( )} } + */ +export async function fetchScorecards (filters = {}) { + const query = { + perPage: 100, + page: 1, + ...filters + } + const response = await axiosInstance.get(`${SCORECARDS_API_URL}?${qs.stringify(query, { encode: false })}`) + return _.get(response, 'data', {}) +} +/** + * Api request for fetching default reviewers + * @param {Object} filters filters for default reviewers + * @returns {Promise<*>} + */ +export async function fetchDefaultReviewers (filters = {}) { + // Use CHALLENGE_API_URL directly for default reviewers endpoint + const response = await axiosInstance.get(`${CHALLENGE_API_URL}/default-reviewers?${qs.stringify(filters, { encode: false })}`) + return _.get(response, 'data', []) +} From d56a1ce219c8f227dd1333f0cdbaf64e3f5547a6 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Tue, 9 Sep 2025 20:34:47 +0530 Subject: [PATCH 04/13] Updated hostname in prod --- config/constants/production.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/constants/production.js b/config/constants/production.js index cb25c033..7e6411fc 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -27,7 +27,7 @@ module.exports = { RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v5/resource-roles`, SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v5/submissions`, REVIEW_TYPE_API_URL: `${PROD_API_HOSTNAME}/v5/reviewTypes`, - SCORECARDS_API_URL: `${DEV_API_HOSTNAME}/v5/scorecards`, //update to use v6 + SCORECARDS_API_URL: `${PROD_API_HOSTNAME}/v5/scorecards`, //update to use v6 SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`, STUDIO_URL: `https://studio.${DOMAIN}`, CONNECT_APP_URL: `https://connect.${DOMAIN}`, From 866d9c87bc5e88fd1007efddb44ae649f15a449b Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Wed, 10 Sep 2025 17:41:18 +0530 Subject: [PATCH 05/13] Added reviewer total to challenge total --- .../ChallengeTotal-Field/index.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js index d968d00c..fa43d787 100644 --- a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js @@ -13,13 +13,28 @@ const ChallengeTotalField = ({ challenge }) => { .map(v => convertDollarToInteger(v, '$')) .reduce((prev, next) => prev + next, 0) } + + // Calculate total reviewer costs + let reviewerTotal = 0 + if (challenge.reviewers) { + reviewerTotal = challenge.reviewers + .filter(r => !r.isAIReviewer) + .reduce((sum, r) => { + const base = r.basePayment || 0 + const count = r.memberReviewerCount || 1 + return sum + (base * count) + }, 0) + } + + // Add reviewer costs to the total challenge total + const totalChallengeCost = (challengeTotal || 0) + reviewerTotal return (
- $ {challengeTotal || 0} + $ {totalChallengeCost}
) From ca279a85192af1aafe21a2e211d6697f7aaf28b4 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Wed, 10 Sep 2025 17:59:31 +0530 Subject: [PATCH 06/13] Show Reviewer section on Challenge view page --- .../ChallengeReviewer-Field/index.js | 268 +++++++++++------- .../ChallengeEditor/ChallengeView/index.js | 1 + .../ChallengeEditor/TextEditor-Field/index.js | 1 + 3 files changed, 167 insertions(+), 103 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 88853f65..15172b48 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -140,7 +140,7 @@ class ChallengeReviewerField extends Component { } renderReviewerForm (reviewer, index) { - const { challenge, metadata } = this.props + const { challenge, metadata, readOnly = false } = this.props const { scorecards = [] } = metadata const validationErrors = this.validateReviewer(reviewer) @@ -148,11 +148,13 @@ class ChallengeReviewerField extends Component {

Reviewer {index + 1}

- this.removeReviewer(index)} - /> + {!readOnly && ( + this.removeReviewer(index)} + /> + )}
{validationErrors.length > 0 && ( @@ -166,69 +168,91 @@ class ChallengeReviewerField extends Component {
- + {readOnly ? ( + {reviewer.isAIReviewer ? 'AI Reviewer' : 'Member Reviewer'} + ) : ( + + )}
- + {readOnly ? ( + + {(() => { + const scorecard = scorecards.find(s => s.id === reviewer.scorecardId) + return scorecard ? `${scorecard.name} - ${scorecard.type} (${scorecard.challengeTrack}) v${scorecard.version}` : 'Not selected' + })()} + + ) : ( + + )}
- + {readOnly ? ( + + {(() => { + const phase = challenge.phases && challenge.phases.find(p => p.id === reviewer.phaseId) + return phase ? (phase.name || `Phase ${phase.phaseId || phase.id}`) : 'Not selected' + })()} + + ) : ( + + )}
@@ -236,34 +260,46 @@ class ChallengeReviewerField extends Component {
- this.updateReviewer(index, 'memberReviewerCount', parseInt(e.target.value))} - /> + {readOnly ? ( + {reviewer.memberReviewerCount || 1} + ) : ( + this.updateReviewer(index, 'memberReviewerCount', parseInt(e.target.value))} + /> + )}
- this.updateReviewer(index, 'basePayment', parseFloat(e.target.value))} - /> + {readOnly ? ( + ${reviewer.basePayment || 0} + ) : ( + this.updateReviewer(index, 'basePayment', parseFloat(e.target.value))} + /> + )}
- this.updateReviewer(index, 'incrementalPayment', parseFloat(e.target.value))} - /> + {readOnly ? ( + ${reviewer.incrementalPayment || 0} + ) : ( + this.updateReviewer(index, 'incrementalPayment', parseFloat(e.target.value))} + /> + )}
)} @@ -271,16 +307,31 @@ class ChallengeReviewerField extends Component {
- + {readOnly ? ( + + {(() => { + const typeMap = { + [REVIEW_OPPORTUNITY_TYPES.REGULAR_REVIEW]: 'Regular Review', + [REVIEW_OPPORTUNITY_TYPES.COMPONENT_DEV_REVIEW]: 'Component Dev Review', + [REVIEW_OPPORTUNITY_TYPES.SPEC_REVIEW]: 'Spec Review', + [REVIEW_OPPORTUNITY_TYPES.ITERATIVE_REVIEW]: 'Iterative Review', + [REVIEW_OPPORTUNITY_TYPES.SCENARIOS_REVIEW]: 'Scenarios Review' + } + return typeMap[reviewer.type] || 'Regular Review' + })()} + + ) : ( + + )}
@@ -288,7 +339,7 @@ class ChallengeReviewerField extends Component { } render () { - const { challenge, metadata, isLoading } = this.props + const { challenge, metadata, isLoading, readOnly = false } = this.props const { error } = this.state const { scorecards = [], defaultReviewers = [] } = metadata const reviewers = challenge.reviewers || [] @@ -327,11 +378,13 @@ class ChallengeReviewerField extends Component {
-
- Configure how this challenge will be reviewed. You can add multiple reviewers including AI and member reviewers. -
+ {!readOnly && ( +
+ Configure how this challenge will be reviewed. You can add multiple reviewers including AI and member reviewers. +
+ )} - {reviewers.length === 0 && ( + {!readOnly && reviewers.length === 0 && (

No reviewers configured. Click "Add Reviewer" to get started.

{this.findDefaultReviewer() && ( @@ -347,6 +400,12 @@ class ChallengeReviewerField extends Component {
)} + {readOnly && reviewers.length === 0 && ( +
+

No reviewers configured for this challenge.

+
+ )} + {reviewers.map((reviewer, index) => this.renderReviewerForm(reviewer, index) )} @@ -373,13 +432,15 @@ class ChallengeReviewerField extends Component {
)} -
- -
+ {!readOnly && ( +
+ +
+ )}
@@ -395,6 +456,7 @@ ChallengeReviewerField.propTypes = { defaultReviewers: PropTypes.array }), isLoading: PropTypes.bool, + readOnly: PropTypes.bool, loadScorecards: PropTypes.func.isRequired, loadDefaultReviewers: PropTypes.func.isRequired } diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 430e22bf..34622a0e 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -241,6 +241,7 @@ const ChallengeView = ({ {/* hide until challenge API change is pushed to PROD https://github.com/topcoder-platform/challenge-api/issues/348 */} {false && )} Date: Thu, 11 Sep 2025 14:06:35 +0530 Subject: [PATCH 07/13] Revert status change --- src/actions/challenges.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 11e52aef..f644d7fc 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -172,11 +172,11 @@ export function loadChallengesByPage ( } if (status !== 'all') { - filters['status'] = !status ? undefined : _.startCase(status.toLowerCase()) + filters['status'] = !status ? undefined : status } if (!dashboard && !filters['status'] && !(_.isInteger(projectId) && projectId > 0)) { - filters['status'] = 'Active' + filters['status'] = 'ACTIVE' } if (selfService) { filters.selfService = true @@ -240,7 +240,7 @@ export function loadChallenges ( if (!_.isEmpty(status)) { filters['status'] = status === '' ? undefined : _.startCase(status.toLowerCase()) } else if (!(_.isInteger(projectId) && projectId > 0)) { - filters['status'] = 'Active' + filters['status'] = 'ACTIVE' } let fetchedChallenges = [] From 445377b5c0b9d3299412108a2e87ea0d754ecebf Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 11 Sep 2025 14:06:47 +0530 Subject: [PATCH 08/13] Updated reviewers to reviewer --- src/components/ChallengeEditor/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 1bdf5dfd..c448a024 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -239,8 +239,8 @@ class ChallengeEditor extends Component { const challengeDetail = { ...challengeData } const isRequiredNda = challengeDetail.terms && _.some(challengeDetail.terms, { id: DEFAULT_NDA_UUID }) const isOpenAdvanceSettings = challengeDetail.groups.length > 0 || isRequiredNda - if (!challengeDetail.reviewer) { - challengeDetail.reviewer = [] + if (!challengeDetail.reviewers) { + challengeDetail.reviewers = [] } setState({ challenge: challengeDetail, From 2cf363fdb5c5df115dd40a9c1572a3ade4037bd1 Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 11 Sep 2025 14:20:49 +0530 Subject: [PATCH 09/13] Changed onUpdateOthers to onUpdateReviewers --- src/components/ChallengeEditor/TextEditor-Field/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ChallengeEditor/TextEditor-Field/index.js b/src/components/ChallengeEditor/TextEditor-Field/index.js index b36ce1df..59e6e353 100644 --- a/src/components/ChallengeEditor/TextEditor-Field/index.js +++ b/src/components/ChallengeEditor/TextEditor-Field/index.js @@ -83,7 +83,7 @@ class TextEditorField extends Component { {showReviewerField && ( )} @@ -143,7 +143,7 @@ TextEditorField.defaultProps = { onUpdateMultiSelect: () => {}, readOnly: false, showReviewerField: false, - onUpdateOthers: () => {} + onUpdateReviewers: () => {} } TextEditorField.propTypes = { @@ -158,7 +158,7 @@ TextEditorField.propTypes = { shouldShowPrivateDescription: PropTypes.bool, readOnly: PropTypes.bool, showReviewerField: PropTypes.bool, - onUpdateOthers: PropTypes.func + onUpdateReviewers: PropTypes.func } export default TextEditorField From e05594d573632341da9230607d3fcfb2a36132ef Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 11 Sep 2025 15:16:27 +0530 Subject: [PATCH 10/13] Renamed methods and props --- .../ChallengeEditor/ChallengeReviewer-Field/index.js | 4 ++-- src/components/ChallengeEditor/ChallengeTotal-Field/index.js | 2 -- src/components/ChallengeEditor/index.js | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 15172b48..c2c7710e 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -24,7 +24,7 @@ class ChallengeReviewerField extends Component { componentDidMount () { this.loadScorecards() - this.isLoadingDefaultReviewers() + this.loadDefaultReviewers() } loadScorecards () { @@ -40,7 +40,7 @@ class ChallengeReviewerField extends Component { loadScorecards(filters) } - isLoadingDefaultReviewers () { + loadDefaultReviewers () { const { challenge, loadDefaultReviewers } = this.props // only load default reviewers if we have typeId and trackId diff --git a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js index fa43d787..9fabb873 100644 --- a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js @@ -14,7 +14,6 @@ const ChallengeTotalField = ({ challenge }) => { .reduce((prev, next) => prev + next, 0) } - // Calculate total reviewer costs let reviewerTotal = 0 if (challenge.reviewers) { reviewerTotal = challenge.reviewers @@ -26,7 +25,6 @@ const ChallengeTotalField = ({ challenge }) => { }, 0) } - // Add reviewer costs to the total challenge total const totalChallengeCost = (challengeTotal || 0) + reviewerTotal return (
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index c448a024..bd1a43d6 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -1860,7 +1860,7 @@ class ChallengeEditor extends Component { onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} showReviewerField - onUpdateOthers={this.onUpdateOthers} + onUpdateReviewers={this.onUpdateOthers} /> {/* hide until challenge API change is pushed to PROD https://github.com/topcoder-platform/challenge-api/issues/348 */} {false && Date: Thu, 11 Sep 2025 20:05:30 +0530 Subject: [PATCH 11/13] Fixed metadata , phase issue, ConvertDollarToInteger --- .../ChallengeReviewer-Field/index.js | 91 +++++++++++-------- .../ChallengeTotal-Field/index.js | 8 +- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index c2c7710e..3b54439b 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -3,9 +3,10 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import cn from 'classnames' import { PrimaryButton, OutlineButton } from '../../Buttons' -import { REVIEW_OPPORTUNITY_TYPES } from '../../../config/constants' +import { REVIEW_OPPORTUNITY_TYPES, VALIDATION_VALUE_TYPE } from '../../../config/constants' import { loadScorecards, loadDefaultReviewers } from '../../../actions/challenges' import styles from './ChallengeReviewer-Field.module.scss' +import { convertDollarToInteger, validateValue } from '../../../util/input-check' class ChallengeReviewerField extends Component { constructor (props) { @@ -69,11 +70,11 @@ class ChallengeReviewerField extends Component { scorecardId: (defaultReviewer && defaultReviewer.scorecardId) || '', isMemberReview: true, memberReviewerCount: (defaultReviewer && defaultReviewer.memberReviewerCount) || 1, - phaseId: (defaultReviewer && defaultReviewer.phaseId) || (firstPhase ? firstPhase.id : ''), - basePayment: (defaultReviewer && defaultReviewer.basePayment) || 0, + phaseId: (defaultReviewer && defaultReviewer.phaseId) || (firstPhase ? (firstPhase.id || firstPhase.phaseId) : ''), + basePayment: (defaultReviewer && defaultReviewer.basePayment) || '0', incrementalPayment: (defaultReviewer && defaultReviewer.incrementalPayment) || 0, type: (defaultReviewer && defaultReviewer.opportunityType) || REVIEW_OPPORTUNITY_TYPES.REGULAR_REVIEW, - isAIReviewer: false + isAIReviewer: Boolean((defaultReviewer && defaultReviewer.isAIReviewer) || false) } const updatedReviewers = currentReviewers.concat([newReviewer]) @@ -98,7 +99,7 @@ class ChallengeReviewerField extends Component { } findDefaultReviewer () { - const { challenge, metadata } = this.props + const { challenge, metadata = {} } = this.props const { defaultReviewers = [] } = metadata if (!challenge || !challenge.trackId || !challenge.typeId) { @@ -113,6 +114,10 @@ class ChallengeReviewerField extends Component { validateReviewer (reviewer) { const errors = [] + if (typeof reviewer.isAIReviewer !== 'boolean') { + errors.push('Reviewer type must be specified') + } + if (!reviewer.scorecardId) { errors.push('Scorecard is required') } @@ -121,11 +126,13 @@ class ChallengeReviewerField extends Component { errors.push('Phase is required') } - if (!reviewer.isAIReviewer && (!reviewer.memberReviewerCount || reviewer.memberReviewerCount < 1)) { - errors.push('Number of reviewers must be at least 1') + const memberCount = parseInt(reviewer.memberReviewerCount) || 1 + if (!reviewer.isAIReviewer && (memberCount < 1 || !Number.isInteger(memberCount))) { + errors.push('Number of reviewers must be a positive integer') } - if (!reviewer.isAIReviewer && (!reviewer.basePayment || reviewer.basePayment < 0)) { + const basePayment = convertDollarToInteger(reviewer.basePayment, '') + if (!reviewer.isAIReviewer && (basePayment < 0)) { errors.push('Base payment must be non-negative') } @@ -140,14 +147,14 @@ class ChallengeReviewerField extends Component { } renderReviewerForm (reviewer, index) { - const { challenge, metadata, readOnly = false } = this.props + const { challenge, metadata = {}, readOnly = false } = this.props const { scorecards = [] } = metadata const validationErrors = this.validateReviewer(reviewer) return (
-

Reviewer {index + 1}

+ {index > 0 &&

Reviewer {index + 1}

} {!readOnly && ( 0 && (
{validationErrors.map((error, i) => ( -
{error}
+
{error}
))}
)} @@ -185,12 +192,12 @@ class ChallengeReviewerField extends Component { updatedReviewers[index] = { scorecardId: currentReviewer.scorecardId, isMemberReview: !isAI, - memberReviewerCount: currentReviewer.memberReviewerCount, + memberReviewerCount: currentReviewer.memberReviewerCount || 1, phaseId: currentReviewer.phaseId, - basePayment: currentReviewer.basePayment, - incrementalPayment: currentReviewer.incrementalPayment, + basePayment: currentReviewer.basePayment || '0', + incrementalPayment: currentReviewer.incrementalPayment || 0, type: currentReviewer.type, - isAIReviewer: isAI + isAIReviewer: Boolean(isAI) } onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) @@ -231,7 +238,9 @@ class ChallengeReviewerField extends Component { {readOnly ? ( {(() => { - const phase = challenge.phases && challenge.phases.find(p => p.id === reviewer.phaseId) + const phase = challenge.phases && challenge.phases.find(p => + (p.id === reviewer.phaseId) || (p.phaseId === reviewer.phaseId) + ) return phase ? (phase.name || `Phase ${phase.phaseId || phase.id}`) : 'Not selected' })()} @@ -242,12 +251,13 @@ class ChallengeReviewerField extends Component { > {challenge.phases && challenge.phases - .filter(phase => - phase.name && - phase.name.toLowerCase().includes('review') - ) + .filter(phase => { + const isReviewPhase = phase.name && phase.name.toLowerCase().includes('review') + const isCurrentlySelected = (phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId) + return isReviewPhase || isCurrentlySelected + }) .map(phase => ( - ))} @@ -267,7 +277,11 @@ class ChallengeReviewerField extends Component { type='number' min='1' value={reviewer.memberReviewerCount || 1} - onChange={(e) => this.updateReviewer(index, 'memberReviewerCount', parseInt(e.target.value))} + onChange={(e) => { + const validatedValue = validateValue(e.target.value, VALIDATION_VALUE_TYPE.INTEGER) + const parsedValue = parseInt(validatedValue) || 1 + this.updateReviewer(index, 'memberReviewerCount', Math.max(1, parsedValue)) + }} /> )}
@@ -275,14 +289,17 @@ class ChallengeReviewerField extends Component {
{readOnly ? ( - ${reviewer.basePayment || 0} + ${reviewer.basePayment || '0'} ) : ( this.updateReviewer(index, 'basePayment', parseFloat(e.target.value))} + value={reviewer.basePayment || '0'} + onChange={(e) => { + const validatedValue = validateValue(e.target.value, VALIDATION_VALUE_TYPE.INTEGER) + this.updateReviewer(index, 'basePayment', validatedValue) + }} /> )}
@@ -339,7 +356,7 @@ class ChallengeReviewerField extends Component { } render () { - const { challenge, metadata, isLoading, readOnly = false } = this.props + const { challenge, metadata = {}, isLoading, readOnly = false } = this.props const { error } = this.state const { scorecards = [], defaultReviewers = [] } = metadata const reviewers = challenge.reviewers || [] @@ -384,7 +401,7 @@ class ChallengeReviewerField extends Component {
)} - {!readOnly && reviewers.length === 0 && ( + {!readOnly && reviewers && reviewers.length === 0 && (

No reviewers configured. Click "Add Reviewer" to get started.

{this.findDefaultReviewer() && ( @@ -400,34 +417,34 @@ class ChallengeReviewerField extends Component {
)} - {readOnly && reviewers.length === 0 && ( + {readOnly && reviewers && reviewers.length === 0 && (

No reviewers configured for this challenge.

)} - {reviewers.map((reviewer, index) => + {reviewers && reviewers.map((reviewer, index) => this.renderReviewerForm(reviewer, index) )} - {reviewers.length > 0 && ( + {reviewers && reviewers.length > 0 && (

Review Summary

Total Member Reviewers: - {reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => sum + (r.memberReviewerCount || 0), 0)} + {reviewers.filter(r => Boolean(r.isAIReviewer) === false).reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 0), 0)}
Total AI Reviewers: - {reviewers.filter(r => r.isAIReviewer).length} + {reviewers.filter(r => Boolean(r.isAIReviewer) === true).length}
Total Review Cost: - ${reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => { - const base = r.basePayment || 0 - const count = r.memberReviewerCount || 1 + ${reviewers.filter(r => Boolean(r.isAIReviewer) === false).reduce((sum, r) => { + const base = convertDollarToInteger(r.basePayment, '') + const count = parseInt(r.memberReviewerCount) || 1 return sum + (base * count) - }, 0).toFixed(2)} + }, 0)}
)} @@ -462,7 +479,7 @@ ChallengeReviewerField.propTypes = { } const mapStateToProps = (state) => ({ - metadata: state.challenges.metadata, + metadata: state.challenges.metadata || {}, isLoading: state.challenges.isLoading }) diff --git a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js index 9fabb873..e0bd5c21 100644 --- a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js @@ -15,12 +15,12 @@ const ChallengeTotalField = ({ challenge }) => { } let reviewerTotal = 0 - if (challenge.reviewers) { + if (challenge.reviewers && Array.isArray(challenge.reviewers)) { reviewerTotal = challenge.reviewers - .filter(r => !r.isAIReviewer) + .filter(r => Boolean(r.isAIReviewer) === false) .reduce((sum, r) => { - const base = r.basePayment || 0 - const count = r.memberReviewerCount || 1 + const base = convertDollarToInteger(r.basePayment, '') + const count = parseInt(r.memberReviewerCount) || 1 return sum + (base * count) }, 0) } From 82aac16772915b752cfb9edcc8abf6cb24dff63d Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 11 Sep 2025 20:08:51 +0530 Subject: [PATCH 12/13] Removed step --- src/components/ChallengeEditor/ChallengeReviewer-Field/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 3b54439b..8dbcaf1e 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -294,7 +294,6 @@ class ChallengeReviewerField extends Component { { const validatedValue = validateValue(e.target.value, VALIDATION_VALUE_TYPE.INTEGER) From c0799c265fd3e31566e47a02b5c455187f302a4d Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Thu, 11 Sep 2025 20:55:27 +0530 Subject: [PATCH 13/13] Fixed minor bugs --- .../ChallengeReviewer-Field/index.js | 27 ++++++++++--------- .../ChallengeTotal-Field/index.js | 4 +-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js index 8dbcaf1e..bd91706e 100644 --- a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js @@ -63,18 +63,21 @@ class ChallengeReviewerField extends Component { // Create a new default reviewer based on track and type const defaultReviewer = this.findDefaultReviewer() - // Get the first available phase if phases exist - const firstPhase = challenge.phases && challenge.phases.length > 0 ? challenge.phases[0] : null + // Get the first available review phase if phases exist + const reviewPhases = challenge.phases && challenge.phases.filter(phase => + phase.name && phase.name.toLowerCase().includes('review') + ) + const firstReviewPhase = reviewPhases && reviewPhases.length > 0 ? reviewPhases[0] : null const newReviewer = { scorecardId: (defaultReviewer && defaultReviewer.scorecardId) || '', isMemberReview: true, memberReviewerCount: (defaultReviewer && defaultReviewer.memberReviewerCount) || 1, - phaseId: (defaultReviewer && defaultReviewer.phaseId) || (firstPhase ? (firstPhase.id || firstPhase.phaseId) : ''), + phaseId: (defaultReviewer && defaultReviewer.phaseId) || (firstReviewPhase ? (firstReviewPhase.id || firstReviewPhase.phaseId) : ''), basePayment: (defaultReviewer && defaultReviewer.basePayment) || '0', incrementalPayment: (defaultReviewer && defaultReviewer.incrementalPayment) || 0, type: (defaultReviewer && defaultReviewer.opportunityType) || REVIEW_OPPORTUNITY_TYPES.REGULAR_REVIEW, - isAIReviewer: Boolean((defaultReviewer && defaultReviewer.isAIReviewer) || false) + isAIReviewer: (defaultReviewer && defaultReviewer.isAIReviewer) || false } const updatedReviewers = currentReviewers.concat([newReviewer]) @@ -131,7 +134,7 @@ class ChallengeReviewerField extends Component { errors.push('Number of reviewers must be a positive integer') } - const basePayment = convertDollarToInteger(reviewer.basePayment, '') + const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '') if (!reviewer.isAIReviewer && (basePayment < 0)) { errors.push('Base payment must be non-negative') } @@ -197,7 +200,7 @@ class ChallengeReviewerField extends Component { basePayment: currentReviewer.basePayment || '0', incrementalPayment: currentReviewer.incrementalPayment || 0, type: currentReviewer.type, - isAIReviewer: Boolean(isAI) + isAIReviewer: isAI } onUpdateReviewers({ field: 'reviewers', value: updatedReviewers }) @@ -253,7 +256,7 @@ class ChallengeReviewerField extends Component { {challenge.phases && challenge.phases .filter(phase => { const isReviewPhase = phase.name && phase.name.toLowerCase().includes('review') - const isCurrentlySelected = (phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId) + const isCurrentlySelected = reviewer.phaseId && ((phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId)) return isReviewPhase || isCurrentlySelected }) .map(phase => ( @@ -431,19 +434,19 @@ class ChallengeReviewerField extends Component {

Review Summary

Total Member Reviewers: - {reviewers.filter(r => Boolean(r.isAIReviewer) === false).reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 0), 0)} + {reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 0), 0)}
Total AI Reviewers: - {reviewers.filter(r => Boolean(r.isAIReviewer) === true).length} + {reviewers.filter(r => r.isAIReviewer).length}
Total Review Cost: - ${reviewers.filter(r => Boolean(r.isAIReviewer) === false).reduce((sum, r) => { - const base = convertDollarToInteger(r.basePayment, '') + ${reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => { + const base = convertDollarToInteger(r.basePayment || '0', '') const count = parseInt(r.memberReviewerCount) || 1 return sum + (base * count) - }, 0)} + }, 0).toFixed(2)}
)} diff --git a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js index e0bd5c21..020ebd4f 100644 --- a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js @@ -17,9 +17,9 @@ const ChallengeTotalField = ({ challenge }) => { let reviewerTotal = 0 if (challenge.reviewers && Array.isArray(challenge.reviewers)) { reviewerTotal = challenge.reviewers - .filter(r => Boolean(r.isAIReviewer) === false) + .filter(r => !r.isAIReviewer) .reduce((sum, r) => { - const base = convertDollarToInteger(r.basePayment, '') + const base = convertDollarToInteger(r.basePayment || '0', '') const count = parseInt(r.memberReviewerCount) || 1 return sum + (base * count) }, 0)