From 2cb995aa1ea237fa2dfe70033be6323c43f2db7c Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 15 Sep 2025 13:51:26 +0530 Subject: [PATCH 1/3] migrate: review opportunity details page to v6 review API migrates review opportunity details page to v6 review api fixes #1761 --- .../page/review-opportunity-details.js | 33 ++++++++++++ .../FailedToLoad/index.jsx | 2 +- .../FailedToLoad/styles.scss | 2 +- .../Header/PhaseList/index.jsx | 4 +- .../ReviewOpportunityDetailsPage/index.jsx | 2 +- .../ReviewOpportunityCard/index.jsx | 4 +- .../containers/ReviewOpportunityDetails.jsx | 46 +++++++++++++--- .../page/review-opportunity-details.js | 48 +++++++++++++++++ src/shared/services/reviewOpportunities.js | 52 +++++++++++++++++++ 9 files changed, 179 insertions(+), 14 deletions(-) diff --git a/src/shared/actions/page/review-opportunity-details.js b/src/shared/actions/page/review-opportunity-details.js index 6b9c75ffa5..4f8f8aa83f 100644 --- a/src/shared/actions/page/review-opportunity-details.js +++ b/src/shared/actions/page/review-opportunity-details.js @@ -4,15 +4,48 @@ import _ from 'lodash'; import { createActions } from 'redux-actions'; +import { getDetails } from '../../services/reviewOpportunities'; + + /* Holds valid values for the tab state. */ export const TABS = { APPLICATIONS: 'APPLICATIONS', CHALLENGE_SPEC: 'CHALLENGE_SPEC', }; +/** + * @static + * @desc Creates an action that signals beginning of loading the review + * opportunity details. + * @return {Action} + */ +function getDetailsInit() {} + +/** + * @static + * @desc Creates an action that gets details of a review opportunity for + * the specified challenge. + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {String} tokenV3=null Optional. Topcoder auth token v3. + * @default test + * @return {Action} + */ +function getDetailsDone(challengeId, opportunityId, tokenV3) { + return getDetails(challengeId, opportunityId, tokenV3) + .then(details => ({ details })) + .catch((error) => { + if (error.status !== 401) { + console.log('Error Getting Review Opportunity Details', error.content || error); + } + return Promise.reject(error.status); + }); +} + export default createActions({ PAGE: { REVIEW_OPPORTUNITY_DETAILS: { + GET_DETAILS_INIT: getDetailsInit, + GET_DETAILS_DONE: getDetailsDone, SELECT_TAB: _.identity, SET_ROLES: _.identity, TOGGLE_APPLY_MODAL: _.identity, diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx index 214a25f1e4..f60f0f30e1 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import './styles.scss'; const FailedToLoad = () => ( -
+

diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss index 1aa3016218..7c13c67016 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss +++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss @@ -1,6 +1,6 @@ @import "~styles/mixins"; -.comtainer { +.container { background: $tc-gray-neutral-dark; width: 100%; display: flex; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx index c0a4db58c7..a258fa12a9 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx @@ -22,9 +22,9 @@ const { formatDuration } = time; * @return {Object} The rendered React element */ const renderPhase = phase => ( -
+
- {phase.type} + {phase.name}
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx index a322f105e9..0e3339ead4 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx @@ -42,7 +42,7 @@ const ReviewOpportunityDetailsPage = ({

- {details.challenge.title} + {details.challenge.name}

diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index d880792b3b..1f7d0eba0a 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -78,7 +78,7 @@ function ReviewOpportunityCard({ ) /* END - DISABLED UNTIL REVIEW OPPORTUNITY RECEIVE UPDATE TO API V5 */ }
{challenge.title} @@ -156,7 +156,7 @@ function ReviewOpportunityCard({
diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index bb40d2f79e..158c258e0f 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -8,13 +8,14 @@ import React from 'react'; import PT from 'prop-types'; import { connect } from 'react-redux'; -import { actions, errors } from 'topcoder-react-lib'; +import { errors } from 'topcoder-react-lib'; import LoadingIndicator from 'components/LoadingIndicator'; import { activeRoleIds } from 'utils/reviewOpportunities'; import pageActions from 'actions/page/review-opportunity-details'; import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage'; import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad'; import termsActions from 'actions/terms'; +import { goToLogin } from 'utils/tc'; const { fireErrorMessage } = errors; @@ -25,6 +26,7 @@ class ReviewOpportunityDetailsContainer extends React.Component { componentDidMount() { const { challengeId, + opportunityId, details, isLoadingDetails, loadDetails, @@ -32,19 +34,40 @@ class ReviewOpportunityDetailsContainer extends React.Component { } = this.props; if (!isLoadingDetails && !details) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } else if (details.challenge.id !== challengeId) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } } handleOnHeaderApply() { const { + isLoggedIn, + isReviewer, openTermsModal, terms, termsFailure, toggleApplyModal, } = this.props; + + if (!isLoggedIn) { + goToLogin('community-app-main'); + return; + } + + if (!isReviewer) { + fireErrorMessage( + 'Permission Required', + + You must have a reviewer role to apply for this review opportunity.{' '} + + Learn how to become a reviewer + . + , + ); + return; + } + if (termsFailure) { fireErrorMessage('Error Getting Terms Details', ''); return; @@ -130,6 +153,8 @@ ReviewOpportunityDetailsContainer.defaultProps = { termsFailure: false, phasesExpanded: false, tokenV3: null, + isLoggedIn: false, + isReviewer: false, }; /** @@ -140,6 +165,7 @@ ReviewOpportunityDetailsContainer.propTypes = { authError: PT.bool, cancelApplications: PT.func.isRequired, challengeId: PT.string.isRequired, + opportunityId: PT.string.isRequired, details: PT.shape(), handle: PT.string.isRequired, isLoadingDetails: PT.bool, @@ -157,6 +183,8 @@ ReviewOpportunityDetailsContainer.propTypes = { toggleRole: PT.func.isRequired, onPhaseExpand: PT.func.isRequired, tokenV3: PT.string, + isLoggedIn: PT.bool, + isReviewer: PT.bool, }; /** @@ -169,12 +197,14 @@ ReviewOpportunityDetailsContainer.propTypes = { const mapStateToProps = (state, ownProps) => { const api = state.reviewOpportunity; const page = state.page.reviewOpportunityDetails; + const queryParams = new URLSearchParams(ownProps.location.search); const { terms } = state; return { authError: api.authError, applyModalOpened: page.applyModalOpened, challengeId: String(ownProps.match.params.challengeId), - details: api.details, + opportunityId: queryParams.get('opportunityId'), + details: page.details, handle: state.auth.user ? state.auth.user.handle : '', isLoadingDetails: api.isLoadingDetails, phasesExpanded: page.phasesExpanded, @@ -184,6 +214,8 @@ const mapStateToProps = (state, ownProps) => { terms: terms.terms, termsFailure: terms.getTermsFailure, tokenV3: state.auth.tokenV3, + isLoggedIn: Boolean(state.auth.user), + isReviewer: _.includes(state.auth.userRoles || [], 'REVIEWER'), }; }; @@ -194,16 +226,16 @@ const mapStateToProps = (state, ownProps) => { * @return {Object} */ function mapDispatchToProps(dispatch) { - const api = actions.reviewOpportunity; + const api = pageActions.page.reviewOpportunityDetails; const page = pageActions.page.reviewOpportunityDetails; return { cancelApplications: (challengeId, roleIds, tokenV3) => { dispatch(api.cancelApplicationsInit()); dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3)); }, - loadDetails: (challengeId, tokenV3) => { + loadDetails: (challengeId, opportunityId, tokenV3) => { dispatch(api.getDetailsInit()); - dispatch(api.getDetailsDone(challengeId, tokenV3)); + dispatch(api.getDetailsDone(challengeId, opportunityId, tokenV3)); }, onPhaseExpand: () => dispatch(page.togglePhasesExpand()), openTermsModal: () => { diff --git a/src/shared/reducers/page/review-opportunity-details.js b/src/shared/reducers/page/review-opportunity-details.js index 39b8127e44..fe9fa08a49 100644 --- a/src/shared/reducers/page/review-opportunity-details.js +++ b/src/shared/reducers/page/review-opportunity-details.js @@ -3,6 +3,52 @@ import { handleActions } from 'redux-actions'; import actions, { TABS } from 'actions/page/review-opportunity-details'; +/** + * Generates a list of unique terms ids required for the open review roles + * with an agreed field + * + * @param {Object} details Review Opportuny details from API + * @return {Array} List of unique terms + */ +function buildRequiredTermsList(details) { + const roles = details.payments.map(payment => payment.role); + + const requiredTerms = _.uniqBy( + details.challenge.terms + // Sometimes roles such as Primary Reviewer have no directly equal + // terms entry. Include the plain Reviewer terms when present as a back-up. + .filter(term => term.role === 'Reviewer' || _.includes(roles, term.role)) + .map(term => _.pick(term, ['id', 'agreed', 'title'])), + term => term.id, + ); + + return requiredTerms || []; +} + + +/** + * Handles REVIEW_OPPORTUNITY/GET__DETAILS_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onGetDetailsDone(state, { payload, error }) { + if (error) { + return { + ...state, + authError: true, + isLoadingDetails: false, + }; + } + + return { + ...state, + details: payload.details, + isLoadingDetails: false, + requiredTerms: buildRequiredTermsList(payload.details), + }; +} + /** * Creates a new reducer. * @param {Object} state Optional. Initial state. @@ -11,6 +57,8 @@ import actions, { TABS } from 'actions/page/review-opportunity-details'; function create(defaultState = {}) { const a = actions.page.reviewOpportunityDetails; return handleActions({ + [a.getDetailsInit]: state => ({ ...state, isLoadingDetails: true }), + [a.getDetailsDone]: onGetDetailsDone, [a.selectTab]: (state, { payload }) => ({ ...state, selectedTab: payload }), [a.setRoles]: (state, { payload }) => ({ ...state, selectedRoles: payload }), [a.toggleApplyModal]: state => ({ ...state, applyModalOpened: !state.applyModalOpened }), diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index ca7748c02f..6bc119e0bb 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -1,4 +1,6 @@ import { config } from 'topcoder-react-utils'; +import _ from 'lodash'; + const v6ApiUrl = config.API.V6; @@ -24,3 +26,53 @@ export default async function getReviewOpportunities(page, pageSize) { return res.json(); } + +/** + * Sync the fields of V3 and V5 for front-end to process successfully + * @param challenge - challenge to normalize + */ +function normalizeChallengePhases(challenge) { + return { + ...challenge, + phases: _.map(challenge.phases, phase => ({ + ...phase, + scheduledStartDate: phase.scheduledStartTime, + scheduledEndDate: phase.scheduledEndTime, + })), + }; +} + + +/** + * Gets the details of the review opportunity for the corresponding challenge + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @return {Promise} Resolves to the api response in JSON. + */ +export async function getDetails(challengeId, opportunityId) { + const getReviewOpportunityUrl = new URL(`${v6ApiUrl}/review-opportunities/${opportunityId}`); + const getChallengeUrl = new URL(`${v6ApiUrl}/challenges/${challengeId}`); + + try { + const [opportunityRes, challengeRes] = await Promise.all([ + fetch(getReviewOpportunityUrl.toString(), { method: 'GET' }), + fetch(getChallengeUrl.toString(), { method: 'GET' }), + ]); + + if (!opportunityRes.ok) { + throw new Error(`Failed to load review opportunity: ${opportunityRes.statusText}`); + } + if (!challengeRes.ok) { + throw new Error(`Failed to load challenge details: ${challengeRes.statusText}`); + } + + const opportunityData = await opportunityRes.json(); + const challengeData = await challengeRes.json(); + + return { + ...opportunityData.result.content, + challenge: normalizeChallengePhases(challengeData), + }; + } catch (err) { + return Promise.reject(err); + } +} From 608b14d7f685bbac2ece0992488f6b0a84af3255 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 15 Sep 2025 14:23:33 +0530 Subject: [PATCH 2/3] fix: ai review feedbacks ai review feedbacks on return types and logging Fixes #1760 --- src/shared/actions/page/review-opportunity-details.js | 9 +++++---- src/shared/services/reviewOpportunities.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/shared/actions/page/review-opportunity-details.js b/src/shared/actions/page/review-opportunity-details.js index 4f8f8aa83f..7bc3a15208 100644 --- a/src/shared/actions/page/review-opportunity-details.js +++ b/src/shared/actions/page/review-opportunity-details.js @@ -3,6 +3,7 @@ */ import _ from 'lodash'; import { createActions } from 'redux-actions'; +import { logger } from 'topcoder-react-lib'; import { getDetails } from '../../services/reviewOpportunities'; @@ -26,16 +27,16 @@ function getDetailsInit() {} * @desc Creates an action that gets details of a review opportunity for * the specified challenge. * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {Number} opportunityId The ID of the review opportunity * @param {String} tokenV3=null Optional. Topcoder auth token v3. - * @default test * @return {Action} */ -function getDetailsDone(challengeId, opportunityId, tokenV3) { - return getDetails(challengeId, opportunityId, tokenV3) +function getDetailsDone(challengeId, opportunityId) { + return getDetails(challengeId, opportunityId) .then(details => ({ details })) .catch((error) => { if (error.status !== 401) { - console.log('Error Getting Review Opportunity Details', error.content || error); + logger.error('Error Getting Review Opportunity Details', error.content || error); } return Promise.reject(error.status); }); diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index 6bc119e0bb..3374cb02c1 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -46,7 +46,7 @@ function normalizeChallengePhases(challenge) { /** * Gets the details of the review opportunity for the corresponding challenge * @param {Number} challengeId The ID of the challenge (not the opportunity id) - * @return {Promise} Resolves to the api response in JSON. + * @return {Object} The combined data of the review opportunity and challenge details */ export async function getDetails(challengeId, opportunityId) { const getReviewOpportunityUrl = new URL(`${v6ApiUrl}/review-opportunities/${opportunityId}`); From 08b27d4987abacae653a6f65cf57245fbc4365cb Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 16 Sep 2025 12:51:31 +0530 Subject: [PATCH 3/3] fix: remove link removes link for how to be a reviewer, we can add it later when process is in place fixes #1761 --- src/shared/containers/ReviewOpportunityDetails.jsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index 158c258e0f..5f01554ae2 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -59,10 +59,7 @@ class ReviewOpportunityDetailsContainer extends React.Component { fireErrorMessage( 'Permission Required', - You must have a reviewer role to apply for this review opportunity.{' '} - - Learn how to become a reviewer - . + You must have a reviewer role to apply for this review opportunity. , ); return;