Skip to content

Commit 65e7035

Browse files
authored
Merge pull request #1712 from topcoder-platform/develop
Prod deploy for recent changes
2 parents 711bab7 + 15eb792 commit 65e7035

5 files changed

Lines changed: 161 additions & 55 deletions

File tree

pnpm-lock.yaml

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
flex-direction: column;
4040
width: 600px;
4141
}
42+
43+
.fieldError {
44+
margin-top: 12px;
45+
}
4246
}
4347
}
4448

src/components/ChallengeEditor/ChallengeReviewer-Field/index.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,11 @@ class ChallengeReviewerField extends Component {
482482
newReviewer.memberReviewerCount = (defaultReviewer && defaultReviewer.memberReviewerCount) || 1
483483
}
484484

485+
// Clear any prior transient error when add succeeds
486+
if (this.state.error) {
487+
this.setState({ error: null })
488+
}
489+
485490
const updatedReviewers = currentReviewers.concat([newReviewer])
486491
onUpdateReviewers({ field: 'reviewers', value: updatedReviewers })
487492
}
@@ -513,6 +518,21 @@ class ChallengeReviewerField extends Component {
513518

514519
// Special handling for phase and count changes
515520
if (field === 'phaseId') {
521+
// Before changing phase, ensure we're not creating a duplicate manual reviewer for the target phase
522+
const targetPhaseId = value
523+
const isCurrentMember = (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false))
524+
if (isCurrentMember) {
525+
const conflict = (currentReviewers || []).some((r, i) => i !== index && (r.isMemberReview !== false) && (r.phaseId === targetPhaseId))
526+
if (conflict) {
527+
const phase = (challenge.phases || []).find(p => (p.id === targetPhaseId) || (p.phaseId === targetPhaseId))
528+
const phaseName = phase ? (phase.name || targetPhaseId) : targetPhaseId
529+
this.setState({
530+
error: `Cannot move manual reviewer to phase '${phaseName}' because a manual reviewer configuration already exists for that phase.`
531+
})
532+
return
533+
}
534+
}
535+
516536
this.handlePhaseChangeWithReassign(index, value)
517537

518538
// update payment based on default reviewer
@@ -632,6 +652,21 @@ class ChallengeReviewerField extends Component {
632652
const currentReviewers = challenge.reviewers || []
633653
const updatedReviewers = currentReviewers.slice()
634654

655+
// Block switching an AI reviewer to a member reviewer if another manual reviewer exists for same phase
656+
if (!isAI) {
657+
const existingReviewer = currentReviewers[index] || {}
658+
const phaseId = existingReviewer.phaseId
659+
const conflict = currentReviewers.some((r, i) => i !== index && (r.isMemberReview !== false) && (r.phaseId === phaseId))
660+
if (conflict) {
661+
const phase = (challenge.phases || []).find(p => (p.id === phaseId) || (p.phaseId === phaseId))
662+
const phaseName = phase ? (phase.name || phaseId) : phaseId
663+
this.setState({
664+
error: `Cannot switch to Member Reviewer: a manual reviewer configuration already exists for phase '${phaseName}'. Increase "Number of Reviewers" on the existing configuration instead.`
665+
})
666+
return
667+
}
668+
}
669+
635670
// Update reviewer type by setting/clearing aiWorkflowId
636671
const currentReviewer = updatedReviewers[index]
637672

@@ -674,6 +709,11 @@ class ChallengeReviewerField extends Component {
674709
this.handleToggleShouldOpen(index, true)
675710
}
676711

712+
// Clear any transient error when successful change is applied
713+
if (this.state.error) {
714+
this.setState({ error: null })
715+
}
716+
677717
onUpdateReviewers({ field: 'reviewers', value: updatedReviewers })
678718
}}
679719
>
@@ -772,10 +812,10 @@ class ChallengeReviewerField extends Component {
772812
const isPostMortemPhase = norm === 'postmortem'
773813
const isCurrentlySelected = reviewer.phaseId && ((phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId)) && !isSubmissionPhase
774814

775-
// Collect phases already assigned to other reviewers (excluding current reviewer)
815+
// Collect phases already assigned to other manual (member) reviewers (excluding current reviewer)
776816
const assignedPhaseIds = new Set(
777817
(challenge.reviewers || [])
778-
.filter((r, i) => i !== index)
818+
.filter((r, i) => i !== index && (r.isMemberReview !== false))
779819
.map(r => r.phaseId)
780820
.filter(id => id !== undefined && id !== null)
781821
)
@@ -1051,6 +1091,11 @@ class ChallengeReviewerField extends Component {
10511091
/>
10521092
</div>
10531093
)}
1094+
{error && !isLoading && (
1095+
<div className={cn(styles.fieldError, styles.error)}>
1096+
{error}
1097+
</div>
1098+
)}
10541099
</div>
10551100
</div>
10561101
</>

src/components/ChallengeEditor/ChallengeView/index.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import {
2323
PHASE_PRODUCT_CHALLENGE_ID_FIELD,
2424
MULTI_ROUND_CHALLENGE_TEMPLATE_ID,
2525
DS_TRACK_ID,
26+
DEV_TRACK_ID,
27+
MARATHON_TYPE_ID,
28+
CHALLENGE_TYPE_ID,
2629
COMMUNITY_APP_URL
2730
} from '../../../config/constants'
2831
import PhaseInput from '../../PhaseInput'
@@ -94,11 +97,33 @@ const ChallengeView = ({
9497
const isTask = _.get(challenge, 'task.isTask', false)
9598
const phases = _.get(challenge, 'phases', [])
9699
const showCheckpointPrizes = _.get(challenge, 'timelineTemplateId') === MULTI_ROUND_CHALLENGE_TEMPLATE_ID
97-
const isDataScience = challenge.trackId === DS_TRACK_ID
98100
const useDashboardData = _.find(challenge.metadata, { name: 'show_data_dashboard' })
99101
const useDashboard = useDashboardData
100102
? (_.isString(useDashboardData.value) && useDashboardData.value === 'true') ||
101103
(_.isBoolean(useDashboardData.value) && useDashboardData.value) : false
104+
const showDashBoard = (() => {
105+
const isSupportedTrack = challenge.trackId === DS_TRACK_ID || challenge.trackId === DEV_TRACK_ID
106+
const isSupportedType = challenge.typeId === MARATHON_TYPE_ID || challenge.typeId === CHALLENGE_TYPE_ID
107+
108+
return (isSupportedTrack && isSupportedType) || Boolean(useDashboardData)
109+
})()
110+
const dashboardToggle = showDashBoard && (
111+
<div className={styles.row}>
112+
<div className={styles.col}>
113+
<label className={styles.fieldTitle} htmlFor='isDashboardEnabled'>Use data dashboard :</label>
114+
</div>
115+
<div className={styles.col}>
116+
<input
117+
name='isDashboardEnabled'
118+
type='checkbox'
119+
id='isDashboardEnabled'
120+
checked={useDashboard}
121+
readOnly
122+
disabled
123+
/>
124+
</div>
125+
</div>
126+
)
102127

103128
return (
104129
<div className={styles.wrapper}>
@@ -138,13 +163,6 @@ const ChallengeView = ({
138163
</a></span>
139164
</div>
140165
</div>
141-
{isDataScience && (
142-
<div className={cn(styles.row, styles.topRow)}>
143-
<div className={styles.col}>
144-
<span><span className={styles.fieldTitle}>Show data dashboard:</span> {useDashboard ? 'Yes' : 'No'}</span>
145-
</div>
146-
</div>
147-
)}
148166
{isTask &&
149167
<AssignedMemberField challenge={challenge} assignedMemberDetails={assignedMemberDetails} readOnly />}
150168
<CopilotField challenge={{
@@ -175,6 +193,7 @@ const ChallengeView = ({
175193
</div>
176194
{openAdvanceSettings && (
177195
<>
196+
{dashboardToggle}
178197
<NDAField beta challenge={challenge} readOnly />
179198
<div className={cn(styles.row, styles.topRow)}>
180199
<div className={styles.col}>

src/components/ChallengeEditor/index.js

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
MILESTONE_STATUS,
2626
PHASE_PRODUCT_CHALLENGE_ID_FIELD,
2727
QA_TRACK_ID, DESIGN_CHALLENGE_TYPES, ROUND_TYPES,
28-
MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID,
28+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID,
2929
CHALLENGE_STATUS,
3030
SKILLS_OPTIONAL_BILLING_ACCOUNT_IDS
3131
} from '../../config/constants'
@@ -148,6 +148,7 @@ class ChallengeEditor extends Component {
148148
this.onSaveChallenge = this.onSaveChallenge.bind(this)
149149
this.getCurrentTemplate = this.getCurrentTemplate.bind(this)
150150
this.onUpdateMetadata = this.onUpdateMetadata.bind(this)
151+
this.shouldShowDashboardSetting = this.shouldShowDashboardSetting.bind(this)
151152
this.getTemplatePhases = this.getTemplatePhases.bind(this)
152153
this.getAvailableTimelineTemplates = this.getAvailableTimelineTemplates.bind(this)
153154
this.autoUpdateChallengeThrottled = _.throttle(this.validateAndAutoUpdateChallenge.bind(this), 3000) // 3s
@@ -669,6 +670,20 @@ class ChallengeEditor extends Component {
669670
this.setState({ challenge: newChallenge })
670671
}
671672

673+
/**
674+
* Determines when the data dashboard toggle should be shown.
675+
*
676+
* @param {Object} challenge the challenge data to evaluate
677+
*/
678+
shouldShowDashboardSetting (challenge = {}) {
679+
const typeId = _.get(challenge, 'typeId')
680+
const metadata = _.get(challenge, 'metadata', [])
681+
const hasDashboardMetadata = _.some(metadata, { name: 'show_data_dashboard' })
682+
const isMarathonMatch = typeId === MARATHON_TYPE_ID
683+
684+
return isMarathonMatch || hasDashboardMetadata
685+
}
686+
672687
/**
673688
* Remove Phase from challenge Phases list
674689
* @param index
@@ -897,11 +912,47 @@ class ChallengeEditor extends Component {
897912
return !(isRequiredMissing || _.isEmpty(this.state.currentTemplate))
898913
}
899914

915+
// Return array of phase names that have more than one manual (member) reviewer configured.
916+
// If none, returns empty array.
917+
getDuplicateManualReviewerPhases () {
918+
const { challenge } = this.state
919+
const reviewers = (challenge && challenge.reviewers) || []
920+
const phases = (challenge && challenge.phases) || []
921+
922+
const counts = {}
923+
reviewers.forEach(r => {
924+
if (r && (r.isMemberReview !== false) && r.phaseId) {
925+
const pid = String(r.phaseId)
926+
counts[pid] = (counts[pid] || 0) + 1
927+
}
928+
})
929+
930+
const duplicatedPhaseIds = Object.keys(counts).filter(pid => counts[pid] > 1)
931+
if (duplicatedPhaseIds.length === 0) return []
932+
933+
return duplicatedPhaseIds.map(pid => {
934+
const p = phases.find(ph => String(ph.phaseId || ph.id) === pid)
935+
return p ? (p.name || pid) : pid
936+
})
937+
}
938+
900939
validateChallenge () {
901940
if (this.isValidChallenge()) {
941+
// Additional validation: block saving draft if there are duplicate manual reviewer configs per phase
942+
const duplicates = this.getDuplicateManualReviewerPhases()
943+
if (duplicates && duplicates.length > 0) {
944+
const message = `Duplicate manual reviewer configuration found for phase(s): ${duplicates.join(', ')}. Only one manual reviewer configuration is allowed per phase.`
945+
this.setState({ hasValidationErrors: true, error: message })
946+
return false
947+
}
948+
949+
if (this.state.error) {
950+
this.setState({ error: null })
951+
}
902952
this.setState({ hasValidationErrors: false })
903953
return true
904954
}
955+
905956
this.setState(prevState => ({
906957
...prevState,
907958
challenge: {
@@ -1095,11 +1146,7 @@ class ChallengeEditor extends Component {
10951146
const { challenge: { name, trackId, typeId, milestoneId, roundType, challengeType, metadata: challengeMetadata } } = this.state
10961147
const { timelineTemplates } = metadata
10971148
const isDesignChallenge = trackId === DES_TRACK_ID
1098-
const isDataScience = trackId === DS_TRACK_ID
1099-
const isChallengeType = typeId === CHALLENGE_TYPE_ID
1100-
const isDevChallenge = trackId === DEV_TRACK_ID
1101-
const isMM = typeId === MARATHON_TYPE_ID
1102-
const showDashBoard = (isDataScience && isChallengeType) || (isDevChallenge && isMM) || (isDevChallenge && isChallengeType)
1149+
const showDashBoard = this.shouldShowDashboardSetting({ trackId, typeId, metadata: challengeMetadata })
11031150

11041151
// indicate that creating process has started
11051152
this.setState({ isSaving: true })
@@ -1754,18 +1801,33 @@ class ChallengeEditor extends Component {
17541801
const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706
17551802
const copilotResources = metadata.members || challengeResources
17561803
const isDesignChallenge = challenge.trackId === DES_TRACK_ID
1757-
const isDevChallenge = challenge.trackId === DEV_TRACK_ID
1758-
const isMM = challenge.typeId === MARATHON_TYPE_ID
17591804
const isChallengeType = challenge.typeId === CHALLENGE_TYPE_ID
17601805
const showRoundType = isDesignChallenge && isChallengeType
17611806
const showCheckpointPrizes = challenge.timelineTemplateId === MULTI_ROUND_CHALLENGE_TEMPLATE_ID
1762-
const showDashBoard = (challenge.trackId === DS_TRACK_ID && isChallengeType) || (isDevChallenge && isMM) || (isDevChallenge && isChallengeType)
17631807
const useDashboardData = _.find(challenge.metadata, { name: 'show_data_dashboard' })
1808+
const showDashBoard = this.shouldShowDashboardSetting(challenge)
17641809

17651810
const useDashboard = useDashboardData
17661811
? (_.isString(useDashboardData.value) && useDashboardData.value === 'true') ||
17671812
(_.isBoolean(useDashboardData.value) && useDashboardData.value) : false
17681813

1814+
const dashboardToggle = showDashBoard && (
1815+
<div className={styles.row}>
1816+
<div className={cn(styles.field, styles.col1)}>
1817+
<label htmlFor='isDashboardEnabled'>Use data dashboard :</label>
1818+
</div>
1819+
<div className={cn(styles.field, styles.col2)}>
1820+
<input
1821+
name='isDashboardEnabled'
1822+
type='checkbox'
1823+
id='isDashboardEnabled'
1824+
checked={useDashboard}
1825+
onChange={(e) => this.onUpdateMetadata('show_data_dashboard', e.target.checked)}
1826+
/>
1827+
</div>
1828+
</div>
1829+
)
1830+
17691831
const workTypes = getDomainTypes(challenge.trackId)
17701832
let filteredTypes = metadata.challengeTypes.filter(type => workTypes.includes(type.abbreviation))
17711833

@@ -1794,22 +1856,7 @@ class ChallengeEditor extends Component {
17941856
}
17951857
<ChallengeNameField challenge={challenge} onUpdateInput={this.onUpdateInput} />
17961858
{
1797-
showDashBoard && (
1798-
<div className={styles.row}>
1799-
<div className={cn(styles.field, styles.col1)}>
1800-
<label htmlFor='isDashboardEnabled'>Use data dashboard :</label>
1801-
</div>
1802-
<div className={cn(styles.field, styles.col2)}>
1803-
<input
1804-
name='isDashboardEnabled'
1805-
type='checkbox'
1806-
id='isDashboardEnabled'
1807-
checked={useDashboard}
1808-
onChange={(e) => this.onUpdateMetadata('show_data_dashboard', e.target.checked)}
1809-
/>
1810-
</div>
1811-
</div>
1812-
)
1859+
dashboardToggle
18131860
}
18141861
{projectDetail.version === 'v4' && <MilestoneField milestones={activeProjectMilestones} onUpdateSelect={this.onUpdateSelect} projectId={projectDetail.id} selectedMilestoneId={selectedMilestoneId} />}
18151862
{useTask && (<DiscussionField hasForum={hasForum} toggleForum={this.toggleForumOnCreate} />)}
@@ -1841,24 +1888,6 @@ class ChallengeEditor extends Component {
18411888
</div>
18421889

18431890
<ChallengeNameField challenge={challenge} onUpdateInput={this.onUpdateInput} />
1844-
{
1845-
showDashBoard && (
1846-
<div className={styles.row}>
1847-
<div className={cn(styles.field, styles.col1)}>
1848-
<label htmlFor='isDashboardEnabled'>Use data dashboard :</label>
1849-
</div>
1850-
<div className={cn(styles.field, styles.col2)}>
1851-
<input
1852-
name='isDashboardEnabled'
1853-
type='checkbox'
1854-
id='isDashboardEnabled'
1855-
checked={useDashboard}
1856-
onChange={(e) => this.onUpdateMetadata('show_data_dashboard', e.target.checked)}
1857-
/>
1858-
</div>
1859-
</div>
1860-
)
1861-
}
18621891
{isTask && (
18631892
<AssignedMemberField
18641893
challenge={challenge}
@@ -1890,6 +1919,7 @@ class ChallengeEditor extends Component {
18901919
</div>
18911920
{isOpenAdvanceSettings && (
18921921
<React.Fragment>
1922+
{dashboardToggle}
18931923
<NDAField challenge={challenge} toggleNdaRequire={this.toggleNdaRequire} />
18941924
{/* remove terms field and use default term */}
18951925
{false && (<TermsField terms={metadata.challengeTerms} challenge={challenge} onUpdateMultiSelect={this.onUpdateMultiSelect} />)}

0 commit comments

Comments
 (0)