diff --git a/config/constants/development.js b/config/constants/development.js
index 438ffea1..2fd8dcea 100644
--- a/config/constants/development.js
+++ b/config/constants/development.js
@@ -27,6 +27,7 @@ module.exports = {
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`,
+ WORKFLOWS_API_URL: `${DEV_API_HOSTNAME}/v6/workflows`,
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 7e6411fc..a1ad5d32 100644
--- a/config/constants/production.js
+++ b/config/constants/production.js
@@ -27,7 +27,8 @@ 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: `${PROD_API_HOSTNAME}/v5/scorecards`, //update to use v6
+ SCORECARDS_API_URL: `${PROD_API_HOSTNAME}/v5/scorecards`, // update to use v6
+ WORKFLOWS_API_URL: `${DEV_API_HOSTNAME}/v5/workflows`, // 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 f644d7fc..74a7252d 100644
--- a/src/actions/challenges.js
+++ b/src/actions/challenges.js
@@ -22,7 +22,8 @@ import {
deleteResource as deleteResourceAPI,
updateChallengeSkillsApi,
fetchDefaultReviewers,
- fetchScorecards
+ fetchScorecards,
+ fetchWorkflows
} from '../services/challenges'
import { searchProfilesByUserIds } from '../services/user'
import {
@@ -817,3 +818,27 @@ export function loadDefaultReviewers (filters = {}) {
}
}
}
+
+/**
+ * Load workflows
+ */
+export function loadWorkflows () {
+ return async (dispatch) => {
+ try {
+ const workflows = await fetchWorkflows()
+ dispatch({
+ type: LOAD_CHALLENGE_METADATA_SUCCESS,
+ metadataKey: 'workflows',
+ metadataValue: workflows || []
+ })
+ } catch (error) {
+ console.error('Error loading workflows:', error)
+ // Return empty array on error to maintain consistency
+ dispatch({
+ type: LOAD_CHALLENGE_METADATA_SUCCESS,
+ metadataKey: 'workflows',
+ metadataValue: []
+ })
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js b/src/components/ChallengeEditor/ChallengeReviewer-Field/index.js
index bd91706e..b45e2353 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_TYPES, VALIDATION_VALUE_TYPE } from '../../../config/constants'
-import { loadScorecards, loadDefaultReviewers } from '../../../actions/challenges'
+import { loadScorecards, loadDefaultReviewers, loadWorkflows } from '../../../actions/challenges'
import styles from './ChallengeReviewer-Field.module.scss'
import { convertDollarToInteger, validateValue } from '../../../util/input-check'
@@ -15,22 +15,45 @@ class ChallengeReviewerField extends Component {
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)
+ this.isAIReviewer = this.isAIReviewer.bind(this)
+ }
+
+ isAIReviewer (reviewer) {
+ return reviewer && (
+ (reviewer.aiWorkflowId && reviewer.aiWorkflowId.trim() !== '') ||
+ (reviewer.isMemberReview === false)
+ )
}
componentDidMount () {
this.loadScorecards()
this.loadDefaultReviewers()
+ this.loadWorkflows()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { challenge } = this.props
+ const prevChallenge = prevProps.challenge
+
+ if (challenge && prevChallenge &&
+ (challenge.type !== prevChallenge.type || challenge.track !== prevChallenge.track)) {
+ this.loadScorecards()
+ }
+
+ if (challenge && prevChallenge &&
+ (challenge.typeId !== prevChallenge.typeId || challenge.trackId !== prevChallenge.trackId)) {
+ this.loadDefaultReviewers()
+ }
}
loadScorecards () {
const { challenge, loadScorecards } = this.props
- // Build query parameters for the scorecard API
+
const filters = {}
// Add challenge track if available
@@ -38,6 +61,11 @@ class ChallengeReviewerField extends Component {
filters.challengeTrack = challenge.track.toUpperCase()
}
+ // Add challenge type if available
+ if (challenge.type) {
+ filters.challengeType = challenge.type
+ }
+
loadScorecards(filters)
}
@@ -56,6 +84,11 @@ class ChallengeReviewerField extends Component {
})
}
+ loadWorkflows () {
+ const { loadWorkflows } = this.props
+ loadWorkflows()
+ }
+
addReviewer () {
const { challenge, onUpdateReviewers } = this.props
const currentReviewers = challenge.reviewers || []
@@ -69,15 +102,40 @@ class ChallengeReviewerField extends Component {
)
const firstReviewPhase = reviewPhases && reviewPhases.length > 0 ? reviewPhases[0] : null
+ const isAIReviewer = this.isAIReviewer(defaultReviewer)
+
+ // For AI reviewers, get scorecardId from the workflow if available
+ let scorecardId = ''
+ if (isAIReviewer) {
+ const { metadata = {} } = this.props
+ const { workflows = [] } = metadata
+ const defaultWorkflowId = defaultReviewer && defaultReviewer.aiWorkflowId
+ if (defaultWorkflowId) {
+ const workflow = workflows.find(w => w.id === defaultWorkflowId)
+ scorecardId = workflow && workflow.scorecardId ? workflow.scorecardId : undefined
+ } else {
+ scorecardId = undefined
+ }
+ } else {
+ scorecardId = (defaultReviewer && defaultReviewer.scorecardId) || ''
+ }
+
const newReviewer = {
- scorecardId: (defaultReviewer && defaultReviewer.scorecardId) || '',
- isMemberReview: true,
- memberReviewerCount: (defaultReviewer && defaultReviewer.memberReviewerCount) || 1,
+ scorecardId,
+ isMemberReview: !isAIReviewer,
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: (defaultReviewer && defaultReviewer.isAIReviewer) || false
+ type: isAIReviewer ? null : ((defaultReviewer && defaultReviewer.opportunityType) || REVIEW_OPPORTUNITY_TYPES.REGULAR_REVIEW)
+ }
+
+ if (isAIReviewer) {
+ newReviewer.aiWorkflowId = (defaultReviewer && defaultReviewer.aiWorkflowId) || ''
+ }
+
+ // Set member-specific fields for member reviewers
+ if (!isAIReviewer) {
+ newReviewer.memberReviewerCount = (defaultReviewer && defaultReviewer.memberReviewerCount) || 1
}
const updatedReviewers = currentReviewers.concat([newReviewer])
@@ -97,6 +155,18 @@ class ChallengeReviewerField extends Component {
const updatedReviewers = currentReviewers.slice()
const fieldUpdate = {}
fieldUpdate[field] = value
+
+ if (field === 'aiWorkflowId') {
+ const { metadata = {} } = this.props
+ const { workflows = [] } = metadata
+ const workflow = workflows.find(w => w.id === value)
+ if (workflow && workflow.scorecardId) {
+ fieldUpdate.scorecardId = workflow.scorecardId
+ } else {
+ fieldUpdate.scorecardId = undefined
+ }
+ }
+
updatedReviewers[index] = Object.assign({}, updatedReviewers[index], fieldUpdate)
onUpdateReviewers({ field: 'reviewers', value: updatedReviewers })
}
@@ -116,29 +186,32 @@ 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')
+ const isAI = this.isAIReviewer(reviewer)
+
+ if (isAI) {
+ if (!reviewer.aiWorkflowId || reviewer.aiWorkflowId.trim() === '') {
+ errors.push('AI Workflow is required')
+ }
+ } else {
+ if (!reviewer.scorecardId) {
+ errors.push('Scorecard is required')
+ }
+
+ const memberCount = parseInt(reviewer.memberReviewerCount) || 1
+ if (memberCount < 1 || !Number.isInteger(memberCount)) {
+ errors.push('Number of reviewers must be a positive integer')
+ }
+
+ const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '')
+ if (basePayment < 0) {
+ errors.push('Base payment must be non-negative')
+ }
}
if (!reviewer.phaseId) {
errors.push('Phase is required')
}
- const memberCount = parseInt(reviewer.memberReviewerCount) || 1
- if (!reviewer.isAIReviewer && (memberCount < 1 || !Number.isInteger(memberCount))) {
- errors.push('Number of reviewers must be a positive integer')
- }
-
- const basePayment = convertDollarToInteger(reviewer.basePayment || '0', '')
- if (!reviewer.isAIReviewer && (basePayment < 0)) {
- errors.push('Base payment must be non-negative')
- }
-
return errors
}
@@ -151,13 +224,13 @@ class ChallengeReviewerField extends Component {
renderReviewerForm (reviewer, index) {
const { challenge, metadata = {}, readOnly = false } = this.props
- const { scorecards = [] } = metadata
+ const { scorecards = [], workflows = [] } = metadata
const validationErrors = this.validateReviewer(reviewer)
return (
- {index > 0 &&
Reviewer {index + 1}
}
+ Reviewer Type {index + 1}
{!readOnly && (
{readOnly ? (
- {reviewer.isAIReviewer ? 'AI Reviewer' : 'Member Reviewer'}
+ {this.isAIReviewer(reviewer) ? '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'
- })()}
-
- ) : (
-
- )}
-
+ {this.isAIReviewer(reviewer) ? (
+
+
+ {readOnly ? (
+
+ {(() => {
+ const workflow = workflows.find(w => w.id === reviewer.aiWorkflowId)
+ return workflow ? workflow.name : 'Not selected'
+ })()}
+
+ ) : (
+
+ )}
+
+ ) : (
+
+
+ {readOnly ? (
+
+ {(() => {
+ const scorecard = scorecards.find(s => s.id === reviewer.scorecardId)
+ return scorecard ? `${scorecard.name} - ${scorecard.type} (${scorecard.challengeTrack}) v${scorecard.version}` : 'Not selected'
+ })()}
+
+ ) : (
+
+ )}
+
+ )}
@@ -255,9 +376,18 @@ class ChallengeReviewerField extends Component {
{challenge.phases && challenge.phases
.filter(phase => {
- const isReviewPhase = phase.name && phase.name.toLowerCase().includes('review')
- const isCurrentlySelected = reviewer.phaseId && ((phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId))
- return isReviewPhase || isCurrentlySelected
+ const phaseName = phase.name ? phase.name.toLowerCase() : ''
+ const isReviewPhase = phaseName.includes('review')
+ const isSubmissionPhase = phaseName.includes('submission')
+ const isCurrentlySelected = reviewer.phaseId && ((phase.id === reviewer.phaseId) || (phase.phaseId === reviewer.phaseId)) && !isSubmissionPhase
+
+ // For AI reviewers, allow both review and submission phases
+ // For member reviewers, only allow review phases even after changing the reviewer type
+ if (this.isAIReviewer(reviewer)) {
+ return (isReviewPhase || isSubmissionPhase) || isCurrentlySelected
+ } else {
+ return isReviewPhase || isCurrentlySelected
+ }
})
.map(phase => (
- {!reviewer.isAIReviewer && (
+ {!this.isAIReviewer(reviewer) && (
@@ -323,36 +453,38 @@ 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'
- })()}
-
- ) : (
-
- )}
+ {!this.isAIReviewer(reviewer) && (
+
+
+
+ {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'
+ })()}
+
+ ) : (
+
+ )}
+
-
+ )}
)
}
@@ -360,7 +492,7 @@ class ChallengeReviewerField extends Component {
render () {
const { challenge, metadata = {}, isLoading, readOnly = false } = this.props
const { error } = this.state
- const { scorecards = [], defaultReviewers = [] } = metadata
+ const { scorecards = [], defaultReviewers = [], workflows = [] } = metadata
const reviewers = challenge.reviewers || []
if (isLoading) {
@@ -376,8 +508,7 @@ class ChallengeReviewerField extends Component {
)
}
- // Only show error if there's a real error, not just missing data
- if (error && !scorecards.length && !defaultReviewers.length) {
+ if (error && !scorecards.length && !defaultReviewers.length && !workflows.length) {
return (
@@ -434,15 +565,15 @@ class ChallengeReviewerField extends Component {
Review Summary
Total Member Reviewers:
- {reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 0), 0)}
+ {reviewers.filter(r => !this.isAIReviewer(r)).reduce((sum, r) => sum + (parseInt(r.memberReviewerCount) || 0), 0)}
Total AI Reviewers:
- {reviewers.filter(r => r.isAIReviewer).length}
+ {reviewers.filter(r => this.isAIReviewer(r)).length}
Total Review Cost:
- ${reviewers.filter(r => !r.isAIReviewer).reduce((sum, r) => {
+ ${reviewers.filter(r => !this.isAIReviewer(r)).reduce((sum, r) => {
const base = convertDollarToInteger(r.basePayment || '0', '')
const count = parseInt(r.memberReviewerCount) || 1
return sum + (base * count)
@@ -472,12 +603,14 @@ ChallengeReviewerField.propTypes = {
onUpdateReviewers: PropTypes.func.isRequired,
metadata: PropTypes.shape({
scorecards: PropTypes.array,
- defaultReviewers: PropTypes.array
+ defaultReviewers: PropTypes.array,
+ workflows: PropTypes.array
}),
isLoading: PropTypes.bool,
readOnly: PropTypes.bool,
loadScorecards: PropTypes.func.isRequired,
- loadDefaultReviewers: PropTypes.func.isRequired
+ loadDefaultReviewers: PropTypes.func.isRequired,
+ loadWorkflows: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
@@ -487,7 +620,8 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = {
loadScorecards,
- loadDefaultReviewers
+ loadDefaultReviewers,
+ loadWorkflows
}
export default connect(mapStateToProps, mapDispatchToProps)(ChallengeReviewerField)
diff --git a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js
index 020ebd4f..63e440d4 100644
--- a/src/components/ChallengeEditor/ChallengeTotal-Field/index.js
+++ b/src/components/ChallengeEditor/ChallengeTotal-Field/index.js
@@ -17,7 +17,13 @@ const ChallengeTotalField = ({ challenge }) => {
let reviewerTotal = 0
if (challenge.reviewers && Array.isArray(challenge.reviewers)) {
reviewerTotal = challenge.reviewers
- .filter(r => !r.isAIReviewer)
+ .filter(r => {
+ const isAI = r && (
+ (r.aiWorkflowId && r.aiWorkflowId.trim() !== '') ||
+ (r.isMemberReview === false)
+ )
+ return !isAI
+ })
.reduce((sum, r) => {
const base = convertDollarToInteger(r.basePayment || '0', '')
const count = parseInt(r.memberReviewerCount) || 1
diff --git a/src/config/constants.js b/src/config/constants.js
index 8955269c..54c041b4 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -240,6 +240,7 @@ export const REVIEW_TYPES = {
export const REVIEW_OPPORTUNITY_TYPES = {
REGULAR_REVIEW: 'REGULAR_REVIEW',
+ COMPONENT_DEV_REVIEW: 'COMPONENT_DEV_REVIEW',
SPEC_REVIEW: 'SPEC_REVIEW',
ITERATIVE_REVIEW: 'ITERATIVE_REVIEW',
SCENARIOS_REVIEW: 'SCENARIOS_REVIEW'
diff --git a/src/services/challenges.js b/src/services/challenges.js
index 1627fc98..84508d37 100644
--- a/src/services/challenges.js
+++ b/src/services/challenges.js
@@ -14,6 +14,7 @@ const {
SUBMISSIONS_API_URL,
REVIEW_TYPE_API_URL,
SCORECARDS_API_URL,
+ WORKFLOWS_API_URL,
GROUPS_API_URL,
TERMS_API_URL,
RESOURCES_API_URL,
@@ -333,3 +334,12 @@ export async function fetchDefaultReviewers (filters = {}) {
const response = await axiosInstance.get(`${CHALLENGE_API_URL}/default-reviewers?${qs.stringify(filters, { encode: false })}`)
return _.get(response, 'data', [])
}
+
+/**
+ * Api request for fetching workflows
+ * @returns {Promise<*>}
+ */
+export async function fetchWorkflows () {
+ const response = await axiosInstance.get(`${WORKFLOWS_API_URL}`)
+ return _.get(response, 'data', {})
+}