Skip to content

Commit 1034c92

Browse files
authored
Merge pull request #3993 from nqviet/feature/bulk-milestone-updates
Milestone Bulk Updates Challenge
2 parents 17de466 + 6214061 commit 1034c92

File tree

8 files changed

+421
-175
lines changed

8 files changed

+421
-175
lines changed

src/api/timelines.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ export function updateMilestone(timelineId, milestoneId, updatedProps) {
5353
.then(resp => resp.data)
5454
}
5555

56+
/**
57+
* Bulk update milestones
58+
*
59+
* @param {Number} timelineId timeline id
60+
* @param {Array<{}>} milestones the timeline's milestones
61+
*
62+
* @returns {Promise} milestones
63+
*/
64+
export function updateMilestones(timelineId, milestones) {
65+
return axios.patch(`${PROJECTS_API_URL}/v5/timelines/${timelineId}/milestones`, milestones)
66+
.then(resp => resp.data)
67+
}
68+
5669
/**
5770
* Get milestone templates by product template id
5871
*

src/config/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ export const UPDATE_PRODUCT_MILESTONE_PENDING = 'UPDATE_PRODUCT_MILESTONE_PE
303303
export const UPDATE_PRODUCT_MILESTONE_FAILURE = 'UPDATE_PRODUCT_MILESTONE_FAILURE'
304304
export const UPDATE_PRODUCT_MILESTONE_SUCCESS = 'UPDATE_PRODUCT_MILESTONE_SUCCESS'
305305

306+
export const BULK_UPDATE_PRODUCT_MILESTONES = 'BULK_UPDATE_PRODUCT_MILESTONES'
307+
export const BULK_UPDATE_PRODUCT_MILESTONES_PENDING = 'BULK_UPDATE_PRODUCT_MILESTONES_PENDING'
308+
export const BULK_UPDATE_PRODUCT_MILESTONES_FAILURE = 'BULK_UPDATE_PRODUCT_MILESTONES_FAILURE'
309+
export const BULK_UPDATE_PRODUCT_MILESTONES_SUCCESS = 'BULK_UPDATE_PRODUCT_MILESTONES_SUCCESS'
310+
306311
export const COMPLETE_PRODUCT_MILESTONE = 'COMPLETE_PRODUCT_MILESTONE'
307312
export const COMPLETE_PRODUCT_MILESTONE_PENDING = 'COMPLETE_PRODUCT_MILESTONE_PENDING'
308313
export const COMPLETE_PRODUCT_MILESTONE_FAILURE = 'COMPLETE_PRODUCT_MILESTONE_FAILURE'

src/helpers/milestoneHelper.js

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _ from 'lodash'
22
import moment from 'moment'
3+
import update from 'react-addons-update'
34

45
import { MILESTONE_STATUS } from '../config/constants'
56
import { MILESTONE_STATUS_TEXT } from '../config/constants'
@@ -41,4 +42,160 @@ export const getProgressPercent = (totalDays, daysLeft) => {
4142
: 100
4243

4344
return progressPercent
44-
}
45+
}
46+
47+
function mergeJsonObjects(targetObj, sourceObj) {
48+
return _.mergeWith({}, targetObj, sourceObj, (target, source) => {
49+
// Overwrite the array
50+
if (_.isArray(source)) {
51+
return source
52+
}
53+
})
54+
}
55+
56+
function updateMilestone(milestone, updatedProps) {
57+
const entityToUpdate = updatedProps
58+
const durationChanged = entityToUpdate.duration && entityToUpdate.duration !== milestone.duration
59+
const statusChanged = entityToUpdate.status && entityToUpdate.status !== milestone.status
60+
const completionDateChanged = entityToUpdate.completionDate
61+
&& !_.isEqual(milestone.completionDate, entityToUpdate.completionDate)
62+
const today = moment.utc().hours(0).minutes(0).seconds(0).milliseconds(0)
63+
64+
// Merge JSON fields
65+
entityToUpdate.details = mergeJsonObjects(milestone.details, entityToUpdate.details)
66+
67+
let actualStartDateCanged = false
68+
// if status has changed
69+
if (statusChanged) {
70+
// if status has changed to be completed, set the compeltionDate if not provided
71+
if (entityToUpdate.status === MILESTONE_STATUS.COMPLETED) {
72+
entityToUpdate.completionDate = entityToUpdate.completionDate ? entityToUpdate.completionDate : today.toISOString()
73+
entityToUpdate.duration = moment.utc(entityToUpdate.completionDate)
74+
.diff(entityToUpdate.actualStartDate, 'days') + 1
75+
}
76+
// if status has changed to be active, set the startDate to today
77+
if (entityToUpdate.status === MILESTONE_STATUS.ACTIVE) {
78+
// NOTE: not updating startDate as activating a milestone should not update the scheduled start date
79+
// entityToUpdate.startDate = today
80+
// should update actual start date
81+
entityToUpdate.actualStartDate = today.toISOString()
82+
actualStartDateCanged = true
83+
}
84+
}
85+
86+
// Updates the end date of the milestone if:
87+
// 1. if duration of the milestone is udpated, update its end date
88+
// OR
89+
// 2. if actual start date is updated, updating the end date of the activated milestone because
90+
// early or late start of milestone, we are essentially changing the end schedule of the milestone
91+
if (durationChanged || actualStartDateCanged) {
92+
const updatedStartDate = actualStartDateCanged ? entityToUpdate.actualStartDate : milestone.startDate
93+
const updatedDuration = _.get(entityToUpdate, 'duration', milestone.duration)
94+
entityToUpdate.endDate = moment.utc(updatedStartDate).add(updatedDuration - 1, 'days').toDate().toISOString()
95+
}
96+
97+
// if completionDate has changed
98+
if (!statusChanged && completionDateChanged) {
99+
entityToUpdate.duration = moment.utc(entityToUpdate.completionDate)
100+
.diff(entityToUpdate.actualStartDate, 'days') + 1
101+
entityToUpdate.status = MILESTONE_STATUS.COMPLETED
102+
}
103+
104+
return update(milestone, {$merge: entityToUpdate})
105+
}
106+
107+
/**
108+
* Cascades endDate/completionDate changes to all milestones with a greater order than the given one.
109+
* @param {Object} origMilestone the original milestone that was updated
110+
* @param {Object} updMilestone the milestone that was updated
111+
* @returns {Promise<void>} a promise that resolves to the last found milestone. If no milestone exists with an
112+
* order greater than the passed <b>updMilestone</b>, the promise will resolve to the passed
113+
* <b>updMilestone</b>
114+
*/
115+
function updateComingMilestones(origMilestone, updMilestone, timelineMilestones) {
116+
// flag to indicate if the milestone in picture, is updated for completionDate field or not
117+
const completionDateChanged = !_.isEqual(origMilestone.completionDate, updMilestone.completionDate)
118+
const today = moment.utc().hours(0).minutes(0).seconds(0).milliseconds(0).toISOString()
119+
// updated milestone's start date, pefers actual start date over scheduled start date
120+
const updMSStartDate = updMilestone.actualStartDate ? updMilestone.actualStartDate : updMilestone.startDate
121+
// calculates schedule end date for the milestone based on start date and duration
122+
let updMilestoneEndDate = moment.utc(updMSStartDate).add(updMilestone.duration - 1, 'days').toDate()
123+
// if the milestone, in context, is completed, overrides the end date to the completion date
124+
updMilestoneEndDate = updMilestone.completionDate ? updMilestone.completionDate : updMilestoneEndDate
125+
126+
const affectedMilestones = timelineMilestones.filter(milestone => milestone.order > updMilestone.order)
127+
const comingMilestones = _.sortBy(affectedMilestones, 'order')
128+
// calculates the schedule start date for the next milestone
129+
let startDate = moment.utc(updMilestoneEndDate).add(1, 'days').toDate().toISOString()
130+
let firstMilestoneFound = false
131+
132+
let updatedTimelineMilestones = timelineMilestones
133+
for (let i = 0; i < comingMilestones.length; i += 1) {
134+
const updateProps = {}
135+
const milestone = comingMilestones[i]
136+
137+
// Update the milestone startDate if different than the iterated startDate
138+
if (!_.isEqual(milestone.startDate, startDate)) {
139+
updateProps.startDate = startDate
140+
updateProps.updatedBy = updMilestone.updatedBy
141+
}
142+
143+
// Calculate the endDate, and update it if different
144+
const endDate = moment.utc(updateProps.startDate || milestone.startDate).add(milestone.duration - 1, 'days').toDate().toISOString()
145+
if (!_.isEqual(milestone.endDate, endDate)) {
146+
updateProps.endDate = endDate
147+
updateProps.updatedBy = updMilestone.updatedBy
148+
}
149+
150+
// if completionDate is alerted, update status of the first non hidden milestone after the current one
151+
if (!firstMilestoneFound && completionDateChanged && !milestone.hidden) {
152+
// activate next milestone
153+
updateProps.status = MILESTONE_STATUS.ACTIVE
154+
updateProps.actualStartDate = today
155+
firstMilestoneFound = true
156+
}
157+
158+
// if milestone is not hidden, update the startDate for the next milestone, otherwise keep the same startDate for next milestone
159+
if (!milestone.hidden) {
160+
// Set the next startDate value to the next day after completionDate if present or the endDate
161+
startDate = moment.utc(milestone.completionDate
162+
? milestone.completionDate
163+
: updateProps.endDate || milestone.endDate).add(1, 'days').toDate().toISOString()
164+
}
165+
166+
const milestoneIdx = updatedTimelineMilestones.findIndex(item => item.id === milestone.id)
167+
updatedTimelineMilestones = update(updatedTimelineMilestones, {[milestoneIdx]: {$merge: updateProps}})
168+
}
169+
170+
return updatedTimelineMilestones
171+
}
172+
173+
function cascadeMilestones(originalMilestone, updatedMilestone, timelineMilestones) {
174+
const original = originalMilestone
175+
const updated = updatedMilestone
176+
177+
// we need to recalculate change in fields because we update some fields before making actual update
178+
const needToCascade = !_.isEqual(original.completionDate, updated.completionDate) // completion date changed
179+
|| original.duration !== updated.duration // duration changed
180+
|| original.actualStartDate !== updated.actualStartDate // actual start date updated
181+
182+
if (needToCascade) {
183+
const updatedMilestones = updateComingMilestones(original, updated, timelineMilestones)
184+
return updatedMilestones
185+
}
186+
187+
return timelineMilestones
188+
}
189+
190+
export const processUpdateMilestone = (milestone, updatedProps, timelineMilestones) => {
191+
let updatedTimelineMilestones
192+
193+
const updatedMilestone = updateMilestone(milestone, updatedProps)
194+
195+
const milestoneIdx = timelineMilestones.findIndex(item => item.id === updatedMilestone.id)
196+
updatedTimelineMilestones = update(timelineMilestones, { [milestoneIdx]: { $set: updatedMilestone } })
197+
198+
updatedTimelineMilestones = cascadeMilestones(milestone, updatedMilestone, updatedTimelineMilestones)
199+
200+
return { updatedMilestone, updatedTimelineMilestones }
201+
}

0 commit comments

Comments
 (0)