Skip to content

Commit 60c420e

Browse files
Merge pull request #7110 from topcoder-platform/PM-1761
migrate: review opportunity details page to v6 review API
2 parents a425bb0 + 08b27d4 commit 60c420e

File tree

9 files changed

+177
-14
lines changed

9 files changed

+177
-14
lines changed

src/shared/actions/page/review-opportunity-details.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,50 @@
33
*/
44
import _ from 'lodash';
55
import { createActions } from 'redux-actions';
6+
import { logger } from 'topcoder-react-lib';
7+
8+
import { getDetails } from '../../services/reviewOpportunities';
9+
610

711
/* Holds valid values for the tab state. */
812
export const TABS = {
913
APPLICATIONS: 'APPLICATIONS',
1014
CHALLENGE_SPEC: 'CHALLENGE_SPEC',
1115
};
1216

17+
/**
18+
* @static
19+
* @desc Creates an action that signals beginning of loading the review
20+
* opportunity details.
21+
* @return {Action}
22+
*/
23+
function getDetailsInit() {}
24+
25+
/**
26+
* @static
27+
* @desc Creates an action that gets details of a review opportunity for
28+
* the specified challenge.
29+
* @param {Number} challengeId The ID of the challenge (not the opportunity id)
30+
* @param {Number} opportunityId The ID of the review opportunity
31+
* @param {String} tokenV3=null Optional. Topcoder auth token v3.
32+
* @return {Action}
33+
*/
34+
function getDetailsDone(challengeId, opportunityId) {
35+
return getDetails(challengeId, opportunityId)
36+
.then(details => ({ details }))
37+
.catch((error) => {
38+
if (error.status !== 401) {
39+
logger.error('Error Getting Review Opportunity Details', error.content || error);
40+
}
41+
return Promise.reject(error.status);
42+
});
43+
}
44+
1345
export default createActions({
1446
PAGE: {
1547
REVIEW_OPPORTUNITY_DETAILS: {
48+
GET_DETAILS_INIT: getDetailsInit,
49+
GET_DETAILS_DONE: getDetailsDone,
1650
SELECT_TAB: _.identity,
1751
SET_ROLES: _.identity,
1852
TOGGLE_APPLY_MODAL: _.identity,

src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import './styles.scss';
33

44
const FailedToLoad = () => (
5-
<div styleName="comtainer">
5+
<div styleName="container">
66
<div styleName="failed-to-load">
77
<h2>
88
<span />

src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@import "~styles/mixins";
22

3-
.comtainer {
3+
.container {
44
background: $tc-gray-neutral-dark;
55
width: 100%;
66
display: flex;

src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const { formatDuration } = time;
2222
* @return {Object} The rendered React element
2323
*/
2424
const renderPhase = phase => (
25-
<div key={`phase-${phase.type}`} styleName={moment().isBetween(phase.scheduledStartDate, phase.scheduledEndDate) ? 'active-phase' : 'inactive-phase'}>
25+
<div key={`phase-${phase.name}`} styleName={moment().isBetween(phase.scheduledStartDate, phase.scheduledEndDate) ? 'active-phase' : 'inactive-phase'}>
2626
<div styleName="type">
27-
{phase.type}
27+
{phase.name}
2828
</div>
2929
<div styleName="date">
3030
<strong>

src/shared/components/ReviewOpportunityDetailsPage/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const ReviewOpportunityDetailsPage = ({
4242

4343
<div styleName="header">
4444
<h1 styleName="challenge-title">
45-
{details.challenge.title}
45+
{details.challenge.name}
4646
</h1>
4747
<div styleName="tags">
4848
<div styleName="review-opportunity-tag">

src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function ReviewOpportunityCard({
7878
) /* END - DISABLED UNTIL REVIEW OPPORTUNITY RECEIVE UPDATE TO API V5 */ }
7979
<div styleName="challenge-details">
8080
<Link
81-
to={`${challengesUrl}/${challenge.id}`}
81+
to={`${challengesUrl}/${opportunity.challengeId}`}
8282
>
8383
{challenge.title}
8484
</Link>
@@ -156,7 +156,7 @@ function ReviewOpportunityCard({
156156
</Tooltip>
157157
</div>
158158
<Link
159-
to={`/challenges/${challenge.legacyId || challenge.id}/review-opportunities`}
159+
to={`/challenges/${opportunity.challengeId}/review-opportunities?opportunityId=${opportunity.id}`}
160160
styleName="register-button"
161161
>
162162
<span>

src/shared/containers/ReviewOpportunityDetails.jsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import React from 'react';
88
import PT from 'prop-types';
99
import { connect } from 'react-redux';
1010

11-
import { actions, errors } from 'topcoder-react-lib';
11+
import { errors } from 'topcoder-react-lib';
1212
import LoadingIndicator from 'components/LoadingIndicator';
1313
import { activeRoleIds } from 'utils/reviewOpportunities';
1414
import pageActions from 'actions/page/review-opportunity-details';
1515
import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage';
1616
import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad';
1717
import termsActions from 'actions/terms';
18+
import { goToLogin } from 'utils/tc';
1819

1920
const { fireErrorMessage } = errors;
2021

@@ -25,26 +26,45 @@ class ReviewOpportunityDetailsContainer extends React.Component {
2526
componentDidMount() {
2627
const {
2728
challengeId,
29+
opportunityId,
2830
details,
2931
isLoadingDetails,
3032
loadDetails,
3133
tokenV3,
3234
} = this.props;
3335

3436
if (!isLoadingDetails && !details) {
35-
loadDetails(challengeId, tokenV3);
37+
loadDetails(challengeId, opportunityId, tokenV3);
3638
} else if (details.challenge.id !== challengeId) {
37-
loadDetails(challengeId, tokenV3);
39+
loadDetails(challengeId, opportunityId, tokenV3);
3840
}
3941
}
4042

4143
handleOnHeaderApply() {
4244
const {
45+
isLoggedIn,
46+
isReviewer,
4347
openTermsModal,
4448
terms,
4549
termsFailure,
4650
toggleApplyModal,
4751
} = this.props;
52+
53+
if (!isLoggedIn) {
54+
goToLogin('community-app-main');
55+
return;
56+
}
57+
58+
if (!isReviewer) {
59+
fireErrorMessage(
60+
'Permission Required',
61+
<span>
62+
You must have a reviewer role to apply for this review opportunity.
63+
</span>,
64+
);
65+
return;
66+
}
67+
4868
if (termsFailure) {
4969
fireErrorMessage('Error Getting Terms Details', '');
5070
return;
@@ -130,6 +150,8 @@ ReviewOpportunityDetailsContainer.defaultProps = {
130150
termsFailure: false,
131151
phasesExpanded: false,
132152
tokenV3: null,
153+
isLoggedIn: false,
154+
isReviewer: false,
133155
};
134156

135157
/**
@@ -140,6 +162,7 @@ ReviewOpportunityDetailsContainer.propTypes = {
140162
authError: PT.bool,
141163
cancelApplications: PT.func.isRequired,
142164
challengeId: PT.string.isRequired,
165+
opportunityId: PT.string.isRequired,
143166
details: PT.shape(),
144167
handle: PT.string.isRequired,
145168
isLoadingDetails: PT.bool,
@@ -157,6 +180,8 @@ ReviewOpportunityDetailsContainer.propTypes = {
157180
toggleRole: PT.func.isRequired,
158181
onPhaseExpand: PT.func.isRequired,
159182
tokenV3: PT.string,
183+
isLoggedIn: PT.bool,
184+
isReviewer: PT.bool,
160185
};
161186

162187
/**
@@ -169,12 +194,14 @@ ReviewOpportunityDetailsContainer.propTypes = {
169194
const mapStateToProps = (state, ownProps) => {
170195
const api = state.reviewOpportunity;
171196
const page = state.page.reviewOpportunityDetails;
197+
const queryParams = new URLSearchParams(ownProps.location.search);
172198
const { terms } = state;
173199
return {
174200
authError: api.authError,
175201
applyModalOpened: page.applyModalOpened,
176202
challengeId: String(ownProps.match.params.challengeId),
177-
details: api.details,
203+
opportunityId: queryParams.get('opportunityId'),
204+
details: page.details,
178205
handle: state.auth.user ? state.auth.user.handle : '',
179206
isLoadingDetails: api.isLoadingDetails,
180207
phasesExpanded: page.phasesExpanded,
@@ -184,6 +211,8 @@ const mapStateToProps = (state, ownProps) => {
184211
terms: terms.terms,
185212
termsFailure: terms.getTermsFailure,
186213
tokenV3: state.auth.tokenV3,
214+
isLoggedIn: Boolean(state.auth.user),
215+
isReviewer: _.includes(state.auth.userRoles || [], 'REVIEWER'),
187216
};
188217
};
189218

@@ -194,16 +223,16 @@ const mapStateToProps = (state, ownProps) => {
194223
* @return {Object}
195224
*/
196225
function mapDispatchToProps(dispatch) {
197-
const api = actions.reviewOpportunity;
226+
const api = pageActions.page.reviewOpportunityDetails;
198227
const page = pageActions.page.reviewOpportunityDetails;
199228
return {
200229
cancelApplications: (challengeId, roleIds, tokenV3) => {
201230
dispatch(api.cancelApplicationsInit());
202231
dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3));
203232
},
204-
loadDetails: (challengeId, tokenV3) => {
233+
loadDetails: (challengeId, opportunityId, tokenV3) => {
205234
dispatch(api.getDetailsInit());
206-
dispatch(api.getDetailsDone(challengeId, tokenV3));
235+
dispatch(api.getDetailsDone(challengeId, opportunityId, tokenV3));
207236
},
208237
onPhaseExpand: () => dispatch(page.togglePhasesExpand()),
209238
openTermsModal: () => {

src/shared/reducers/page/review-opportunity-details.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@ import { handleActions } from 'redux-actions';
33

44
import actions, { TABS } from 'actions/page/review-opportunity-details';
55

6+
/**
7+
* Generates a list of unique terms ids required for the open review roles
8+
* with an agreed field
9+
*
10+
* @param {Object} details Review Opportuny details from API
11+
* @return {Array} List of unique terms
12+
*/
13+
function buildRequiredTermsList(details) {
14+
const roles = details.payments.map(payment => payment.role);
15+
16+
const requiredTerms = _.uniqBy(
17+
details.challenge.terms
18+
// Sometimes roles such as Primary Reviewer have no directly equal
19+
// terms entry. Include the plain Reviewer terms when present as a back-up.
20+
.filter(term => term.role === 'Reviewer' || _.includes(roles, term.role))
21+
.map(term => _.pick(term, ['id', 'agreed', 'title'])),
22+
term => term.id,
23+
);
24+
25+
return requiredTerms || [];
26+
}
27+
28+
29+
/**
30+
* Handles REVIEW_OPPORTUNITY/GET__DETAILS_DONE action.
31+
* @param {Object} state
32+
* @param {Object} action Payload will be JSON from api call
33+
* @return {Object} New state
34+
*/
35+
function onGetDetailsDone(state, { payload, error }) {
36+
if (error) {
37+
return {
38+
...state,
39+
authError: true,
40+
isLoadingDetails: false,
41+
};
42+
}
43+
44+
return {
45+
...state,
46+
details: payload.details,
47+
isLoadingDetails: false,
48+
requiredTerms: buildRequiredTermsList(payload.details),
49+
};
50+
}
51+
652
/**
753
* Creates a new reducer.
854
* @param {Object} state Optional. Initial state.
@@ -11,6 +57,8 @@ import actions, { TABS } from 'actions/page/review-opportunity-details';
1157
function create(defaultState = {}) {
1258
const a = actions.page.reviewOpportunityDetails;
1359
return handleActions({
60+
[a.getDetailsInit]: state => ({ ...state, isLoadingDetails: true }),
61+
[a.getDetailsDone]: onGetDetailsDone,
1462
[a.selectTab]: (state, { payload }) => ({ ...state, selectedTab: payload }),
1563
[a.setRoles]: (state, { payload }) => ({ ...state, selectedRoles: payload }),
1664
[a.toggleApplyModal]: state => ({ ...state, applyModalOpened: !state.applyModalOpened }),

src/shared/services/reviewOpportunities.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { config } from 'topcoder-react-utils';
2+
import _ from 'lodash';
3+
24

35
const v6ApiUrl = config.API.V6;
46

@@ -24,3 +26,53 @@ export default async function getReviewOpportunities(page, pageSize) {
2426

2527
return res.json();
2628
}
29+
30+
/**
31+
* Sync the fields of V3 and V5 for front-end to process successfully
32+
* @param challenge - challenge to normalize
33+
*/
34+
function normalizeChallengePhases(challenge) {
35+
return {
36+
...challenge,
37+
phases: _.map(challenge.phases, phase => ({
38+
...phase,
39+
scheduledStartDate: phase.scheduledStartTime,
40+
scheduledEndDate: phase.scheduledEndTime,
41+
})),
42+
};
43+
}
44+
45+
46+
/**
47+
* Gets the details of the review opportunity for the corresponding challenge
48+
* @param {Number} challengeId The ID of the challenge (not the opportunity id)
49+
* @return {Object} The combined data of the review opportunity and challenge details
50+
*/
51+
export async function getDetails(challengeId, opportunityId) {
52+
const getReviewOpportunityUrl = new URL(`${v6ApiUrl}/review-opportunities/${opportunityId}`);
53+
const getChallengeUrl = new URL(`${v6ApiUrl}/challenges/${challengeId}`);
54+
55+
try {
56+
const [opportunityRes, challengeRes] = await Promise.all([
57+
fetch(getReviewOpportunityUrl.toString(), { method: 'GET' }),
58+
fetch(getChallengeUrl.toString(), { method: 'GET' }),
59+
]);
60+
61+
if (!opportunityRes.ok) {
62+
throw new Error(`Failed to load review opportunity: ${opportunityRes.statusText}`);
63+
}
64+
if (!challengeRes.ok) {
65+
throw new Error(`Failed to load challenge details: ${challengeRes.statusText}`);
66+
}
67+
68+
const opportunityData = await opportunityRes.json();
69+
const challengeData = await challengeRes.json();
70+
71+
return {
72+
...opportunityData.result.content,
73+
challenge: normalizeChallengePhases(challengeData),
74+
};
75+
} catch (err) {
76+
return Promise.reject(err);
77+
}
78+
}

0 commit comments

Comments
 (0)