Skip to content

Commit 2cd8f6b

Browse files
authored
Merge pull request #1222 from topcoder-platform/feature/linking-challenge-milestone
[DEV] Milestone Challenge Linking
2 parents 76c6d99 + 21c536c commit 2cd8f6b

File tree

14 files changed

+407
-50
lines changed

14 files changed

+407
-50
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ workflows:
150150
context : org-global
151151
filters: &filters-dev
152152
branches:
153-
only: ['develop', 'feature/bug-bash-july', 'feature/test-automation']
153+
only: ['develop', 'feature/linking-challenge-milestone']
154154

155155
# Production builds are exectuted only on tagged commits to the
156156
# master branch.

src/actions/challenges.js

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
LOAD_CHALLENGE_RESOURCES
5050
} from '../config/constants'
5151
import { loadProject } from './projects'
52+
import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '../services/projects'
5253

5354
/**
5455
* Member challenges related redux actions
@@ -203,16 +204,34 @@ export function loadGroupDetails (groupIds) {
203204
*
204205
* @param {String} challengeId challenge id
205206
* @param {Object} challengeDetails challenge data
206-
*
207+
* @param {String} projectId project id
207208
* @returns {Promise<{ type: string, challengeDetails: object }>} action object
208209
*/
209-
export function updateChallengeDetails (challengeId, challengeDetails) {
210+
export function updateChallengeDetails (challengeId, challengeDetails, projectId) {
210211
return async (dispatch) => {
211212
dispatch({
212213
type: UPDATE_CHALLENGE_DETAILS_PENDING
213214
})
214215

215-
return updateChallenge(challengeId, challengeDetails).then((challenge) => {
216+
const milestoneId = challengeDetails.milestoneId
217+
// Check if milestone is deleted or updated
218+
const hasMilestone = _.has(challengeDetails, 'milestoneId')
219+
220+
if (hasMilestone) {
221+
delete challengeDetails.milestoneId
222+
}
223+
return updateChallenge(challengeId, challengeDetails).then(async challenge => {
224+
if (hasMilestone) {
225+
if (milestoneId && milestoneId !== -1) {
226+
await saveChallengeAsPhaseProduct(projectId, milestoneId, challengeId)
227+
challenge.milestoneId = milestoneId
228+
} else {
229+
await removeChallengeFromPhaseProduct(projectId, challengeId)
230+
challenge.milestoneId = milestoneId
231+
}
232+
}
233+
return challenge
234+
}).then((challenge) => {
216235
return dispatch({
217236
type: UPDATE_CHALLENGE_DETAILS_SUCCESS,
218237
challengeDetails: challenge
@@ -231,26 +250,39 @@ export function updateChallengeDetails (challengeId, challengeDetails) {
231250
* Create a new challenge
232251
*
233252
* @param {Object} challengeDetails challenge data
253+
* @param {String} projectId project id
234254
*
235255
* @returns {Promise<{ type: string, challengeDetails: object }>} action object
236256
*/
237-
export function createChallenge (challengeDetails) {
257+
export function createChallenge (challengeDetails, projectId) {
258+
console.log(challengeDetails)
238259
return async (dispatch) => {
239260
dispatch({
240261
type: CREATE_CHALLENGE_PENDING
241262
})
242-
243-
return createChallengeAPI(challengeDetails).then((challenge) => {
244-
return dispatch({
245-
type: CREATE_CHALLENGE_SUCCESS,
246-
challengeDetails: challenge
263+
const milestoneId = challengeDetails.milestoneId
264+
if (milestoneId) {
265+
delete challengeDetails.milestoneId
266+
}
267+
return createChallengeAPI(challengeDetails)
268+
.then(async challenge => {
269+
if (milestoneId && milestoneId !== -1) {
270+
await saveChallengeAsPhaseProduct(projectId, milestoneId, challenge.id, true)
271+
challenge.milestoneId = milestoneId
272+
}
273+
return challenge
247274
})
248-
}).catch((e) => {
249-
dispatch({
250-
type: CREATE_CHALLENGE_FAILURE,
251-
error: e
275+
.then((challenge) => {
276+
return dispatch({
277+
type: CREATE_CHALLENGE_SUCCESS,
278+
challengeDetails: challenge
279+
})
280+
}).catch((e) => {
281+
dispatch({
282+
type: CREATE_CHALLENGE_FAILURE,
283+
error: e
284+
})
252285
})
253-
})
254286
}
255287
}
256288

@@ -261,16 +293,32 @@ export function createChallenge (challengeDetails) {
261293
*
262294
* @param {String} challengeId challenge id
263295
* @param {Object} partialChallengeDetails partial challenge data
264-
*
296+
* @param {String} projectId project id
265297
* @returns {Promise<{ type: string, challengeDetails: object }>} action object
266298
*/
267-
export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDetails) {
299+
export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDetails, projectId) {
268300
return async (dispatch) => {
269301
dispatch({
270302
type: UPDATE_CHALLENGE_DETAILS_PENDING
271303
})
272-
273-
return patchChallenge(challengeId, partialChallengeDetails).then((challenge) => {
304+
const milestoneId = partialChallengeDetails.milestoneId
305+
// Check if milestone is deleted or updated
306+
const hasMilestone = _.has(partialChallengeDetails, 'milestoneId')
307+
if (hasMilestone) {
308+
delete partialChallengeDetails.milestoneId
309+
}
310+
return patchChallenge(challengeId, partialChallengeDetails).then(async challenge => {
311+
if (hasMilestone) {
312+
if (milestoneId && milestoneId !== -1) {
313+
await saveChallengeAsPhaseProduct(projectId, milestoneId, challenge.id)
314+
challenge.milestoneId = milestoneId
315+
} else {
316+
await removeChallengeFromPhaseProduct(projectId, challengeId)
317+
challenge.milestoneId = milestoneId
318+
}
319+
}
320+
return challenge
321+
}).then((challenge) => {
274322
return dispatch({
275323
type: UPDATE_CHALLENGE_DETAILS_SUCCESS,
276324
challengeDetails: challenge
@@ -284,13 +332,15 @@ export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDe
284332
}
285333
}
286334

287-
export function deleteChallenge (challengeId) {
335+
export function deleteChallenge (challengeId, projectId) {
288336
return async (dispatch) => {
289337
dispatch({
290338
type: DELETE_CHALLENGE_PENDING
291339
})
292-
293-
return deleteChallengeAPI(challengeId).then((challenge) => {
340+
return deleteChallengeAPI(challengeId).then(async challenge => {
341+
await removeChallengeFromPhaseProduct(projectId, challengeId)
342+
return challenge
343+
}).then((challenge) => {
294344
return dispatch({
295345
type: DELETE_CHALLENGE_SUCCESS,
296346
challengeDetails: challenge

src/actions/projects.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {
22
LOAD_PROJECT_BILLING_ACCOUNT,
33
LOAD_CHALLENGE_MEMBERS_SUCCESS,
4-
LOAD_PROJECT_DETAILS
4+
LOAD_PROJECT_DETAILS,
5+
LOAD_PROJECT_PHASES
56
} from '../config/constants'
6-
import { fetchProjectById, fetchBillingAccount } from '../services/projects'
7+
import { fetchProjectById, fetchBillingAccount, fetchProjectPhases } from '../services/projects'
78

89
/**
910
* Loads project details
@@ -27,6 +28,12 @@ export function loadProject (projectId) {
2728
payload: fetchBillingAccount(projectId)
2829
})
2930

31+
// Loads project phases
32+
dispatch({
33+
type: LOAD_PROJECT_PHASES,
34+
payload: fetchProjectPhases(projectId)
35+
})
36+
3037
return project
3138
})
3239
})

src/components/Buttons/PrimaryButton/PrimaryButton.module.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@
5757
}
5858
}
5959

60+
&.successDark {
61+
@include roboto-bold;
62+
background-color: $tc-green-50;
63+
&:disabled {
64+
cursor: default;
65+
background-color: $inactive;
66+
}
67+
}
68+
6069
/* this style just visually simulates the disable status of the button */
6170
&.disabled {
6271
cursor: default;

src/components/ChallengeEditor/ChallengeView/index.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import AssignedMemberField from '../AssignedMember-Field'
2121
import { getResourceRoleByName } from '../../../util/tc'
2222
import { isBetaMode } from '../../../util/cookie'
2323
import { loadGroupDetails } from '../../../actions/challenges'
24-
import { REVIEW_TYPES } from '../../../config/constants'
24+
import { REVIEW_TYPES, CONNECT_APP_URL, PHASE_PRODUCT_CHALLENGE_ID_FIELD } from '../../../config/constants'
2525

2626
const ChallengeView = ({
2727
projectDetail,
@@ -36,10 +36,17 @@ const ChallengeView = ({
3636
assignedMemberDetails,
3737
enableEdit,
3838
onLaunchChallenge,
39-
onCloseTask
39+
onCloseTask,
40+
projectPhases
4041
}) => {
4142
const selectedType = _.find(metadata.challengeTypes, { id: challenge.typeId })
4243
const challengeTrack = _.find(metadata.challengeTracks, { id: challenge.trackId })
44+
const selectedMilestone = challenge.milestoneId
45+
? _.find(projectPhases, phase => phase.id === challenge.milestoneId)
46+
: _.find(projectPhases,
47+
phase => _.find(_.get(phase, 'products', []),
48+
product => _.get(product, PHASE_PRODUCT_CHALLENGE_ID_FIELD) === challengeId
49+
))
4350

4451
const [openAdvanceSettings, setOpenAdvanceSettings] = useState(false)
4552
const [groups, setGroups] = useState('')
@@ -80,6 +87,7 @@ const ChallengeView = ({
8087
if (isLoading || _.isEmpty(metadata.challengePhases) || challenge.id !== challengeId) return <Loader />
8188
const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706
8289
const isTask = _.get(challenge, 'task.isTask', false)
90+
8391
return (
8492
<div className={styles.wrapper}>
8593
<div className={styles.container}>
@@ -94,6 +102,16 @@ const ChallengeView = ({
94102
}} />
95103
</span>
96104
</div>
105+
{selectedMilestone &&
106+
<div className={styles.col}>
107+
<span><span className={styles.fieldTitle}>Milestone:</span> {selectedMilestone ? (
108+
<a href={`${CONNECT_APP_URL}/projects/${projectDetail.id}`} target='_blank'
109+
rel='noopener noreferrer'>
110+
{selectedMilestone.name}
111+
</a>
112+
) : ''}</span>
113+
</div>
114+
}
97115
<div className={styles.col}>
98116
<span className={styles.fieldTitle}>Track:</span>
99117
<Track disabled type={challengeTrack} isActive key={challenge.trackId} onUpdateOthers={() => {}} />
@@ -111,13 +129,15 @@ const ChallengeView = ({
111129
<span><span className={styles.fieldTitle}>Challenge Name:</span> {challenge.name}</span>
112130
</div>
113131
</div>
114-
{isTask && <AssignedMemberField challenge={challenge} assignedMemberDetails={assignedMemberDetails} readOnly /> }
132+
{isTask &&
133+
<AssignedMemberField challenge={challenge} assignedMemberDetails={assignedMemberDetails} readOnly />}
115134
<CopilotField challenge={{
116135
copilot
117136
}} copilots={metadata.members} readOnly />
118137
<div className={cn(styles.row, styles.topRow)}>
119138
<div className={styles.col}>
120-
<span><span className={styles.fieldTitle}>Review Type:</span> {isCommunity ? 'Community' : 'Internal'}</span>
139+
<span><span
140+
className={styles.fieldTitle}>Review Type:</span> {isCommunity ? 'Community' : 'Internal'}</span>
121141
</div>
122142
</div>
123143
{isInternal && reviewer && (<div className={cn(styles.row, styles.topRow)}>
@@ -175,7 +195,7 @@ const ChallengeView = ({
175195
/>
176196
</div>
177197
}
178-
{ showTimeline && (
198+
{showTimeline && (
179199
<ChallengeScheduleField
180200
templates={metadata.timelineTemplates}
181201
challengePhases={metadata.challengePhases}
@@ -238,7 +258,8 @@ ChallengeView.propTypes = {
238258
assignedMemberDetails: PropTypes.shape(),
239259
enableEdit: PropTypes.bool,
240260
onLaunchChallenge: PropTypes.func,
241-
onCloseTask: PropTypes.func
261+
onCloseTask: PropTypes.func,
262+
projectPhases: PropTypes.arrayOf(PropTypes.object)
242263
}
243264

244265
export default withRouter(ChallengeView)

src/components/ChallengeEditor/ChallengeViewTabs/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ const ChallengeViewTabs = ({
4141
enableEdit,
4242
onLaunchChallenge,
4343
cancelChallenge,
44-
onCloseTask
44+
onCloseTask,
45+
projectPhases
4546
}) => {
4647
const [selectedTab, setSelectedTab] = useState(0)
4748

@@ -203,6 +204,7 @@ const ChallengeViewTabs = ({
203204
enableEdit={enableEdit}
204205
onLaunchChallenge={onLaunchChallenge}
205206
onCloseTask={onCloseTask}
207+
projectPhases={projectPhases}
206208
/>
207209
)}
208210
{selectedTab === 1 && (
@@ -238,7 +240,8 @@ ChallengeViewTabs.propTypes = {
238240
enableEdit: PropTypes.bool,
239241
onLaunchChallenge: PropTypes.func,
240242
cancelChallenge: PropTypes.func.isRequired,
241-
onCloseTask: PropTypes.func
243+
onCloseTask: PropTypes.func,
244+
projectPhases: PropTypes.arrayOf(PropTypes.object)
242245
}
243246

244247
export default ChallengeViewTabs
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@import "../../../styles/includes";
2+
3+
.row {
4+
box-sizing: border-box;
5+
display: flex;
6+
flex-direction: row;
7+
margin: 30px 30px 0 30px;
8+
align-content: space-between;
9+
justify-content: flex-start;
10+
11+
.field {
12+
@include upto-sm {
13+
display: block;
14+
padding-bottom: 10px;
15+
}
16+
17+
label {
18+
@include roboto-bold();
19+
20+
font-size: 16px;
21+
line-height: 19px;
22+
font-weight: 500;
23+
color: $tc-gray-80;
24+
}
25+
26+
&.col1 {
27+
max-width: 185px;
28+
min-width: 185px;
29+
margin-right: 14px;
30+
white-space: nowrap;
31+
display: flex;
32+
align-items: center;
33+
flex-grow: 1;
34+
35+
span {
36+
color: $tc-red;
37+
}
38+
}
39+
40+
&.col2.error {
41+
color: $tc-red;
42+
margin-top: -25px;
43+
}
44+
&.col2 {
45+
align-self: flex-end;
46+
width: 80%;
47+
margin-bottom: auto;
48+
margin-top: auto;
49+
display: flex;
50+
flex-direction: row;
51+
max-width: 600px;
52+
min-width: 600px;
53+
}
54+
55+
&.manageLink {
56+
margin: 2px 12px;
57+
text-decoration: none;
58+
font-size: 12px;
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)