Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/components/Layout/Layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,5 @@
&[data-route*='new-project'] {
background-color: $tc-gray-neutral-light;
}
&[data-route*='add-phase'] {
background-color: $tc-gray-neutral-light;
}
}
}
6 changes: 0 additions & 6 deletions src/components/TopBar/TopBarContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ class TopBarContainer extends React.Component {

render() {

const location = this.props.location.pathname
if (location && (location.substr(location.lastIndexOf('/') + 1) === 'add-phase')) {
return (
<div />
)
}
const { user, toolbar, userRoles } = this.props
const userHandle = _.get(user, 'handle')
const bigPhotoURL = _.get(user, 'photoURL')
Expand Down
8 changes: 8 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export const SET_PROJECTS_INFINITE_AUTOLOAD = 'SET_PROJECTS_INFINITE_AUTOLOAD'
export const SET_PROJECTS_LIST_VIEW = 'SET_PROJECTS_LIST_VIEW'


// project phases and timeline and milestones
export const CREATE_PROJECT_PHASE_TIMELINE_MILESTONES = 'CREATE_PROJECT_PHASE_TIMELINE_MILESTONES'
export const CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE = 'CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE'
export const CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS = 'CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS'
export const CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING = 'CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING'


// Delete project
export const DELETE_PROJECT = 'DELETE_PROJECT'
export const DELETE_PROJECT_PENDING = 'DELETE_PROJECT_PENDING'
Expand Down Expand Up @@ -892,6 +899,7 @@ export const POSTS_BUNDLE_TIME_DIFF = 1000 * 60 * 10 // 10 min difference
export const MILESTONE_STATUS = {
UNPLANNED: 'in_review',
PLANNED: 'reviewed',
DRAFT: 'draft',
ACTIVE: 'active',
BLOCKED: 'paused',
COMPLETED: 'completed',
Expand Down
27 changes: 7 additions & 20 deletions src/helpers/milestoneHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function mergeJsonObjects(targetObj, sourceObj) {

function updateMilestone(milestone, updatedProps) {
const entityToUpdate = updatedProps
const durationChanged = entityToUpdate.duration && entityToUpdate.duration !== milestone.duration

const statusChanged = entityToUpdate.status && entityToUpdate.status !== milestone.status
const completionDateChanged = entityToUpdate.completionDate
&& !_.isEqual(milestone.completionDate, entityToUpdate.completionDate)
Expand All @@ -64,40 +64,23 @@ function updateMilestone(milestone, updatedProps) {
// Merge JSON fields
entityToUpdate.details = mergeJsonObjects(milestone.details, entityToUpdate.details)

let actualStartDateCanged = false
// if status has changed
if (statusChanged) {
// if status has changed to be completed, set the compeltionDate if not provided
if (entityToUpdate.status === MILESTONE_STATUS.COMPLETED) {
entityToUpdate.completionDate = entityToUpdate.completionDate ? entityToUpdate.completionDate : today.toISOString()
entityToUpdate.duration = moment.utc(entityToUpdate.completionDate)
.diff(entityToUpdate.actualStartDate, 'days') + 1
}
// if status has changed to be active, set the startDate to today
if (entityToUpdate.status === MILESTONE_STATUS.ACTIVE) {
// NOTE: not updating startDate as activating a milestone should not update the scheduled start date
// entityToUpdate.startDate = today
// should update actual start date
entityToUpdate.actualStartDate = today.toISOString()
actualStartDateCanged = true
}
}

// Updates the end date of the milestone if:
// 1. if duration of the milestone is udpated, update its end date
// OR
// 2. if actual start date is updated, updating the end date of the activated milestone because
// early or late start of milestone, we are essentially changing the end schedule of the milestone
if (durationChanged || actualStartDateCanged) {
const updatedStartDate = actualStartDateCanged ? entityToUpdate.actualStartDate : milestone.startDate
const updatedDuration = _.get(entityToUpdate, 'duration', milestone.duration)
entityToUpdate.endDate = moment.utc(updatedStartDate).add(updatedDuration - 1, 'days').toDate().toISOString()
}

// if completionDate has changed
if (!statusChanged && completionDateChanged) {
entityToUpdate.duration = moment.utc(entityToUpdate.completionDate)
.diff(entityToUpdate.actualStartDate, 'days') + 1
entityToUpdate.status = MILESTONE_STATUS.COMPLETED
}

Expand Down Expand Up @@ -141,7 +124,7 @@ function updateComingMilestones(origMilestone, updMilestone, timelineMilestones)
}

// Calculate the endDate, and update it if different
const endDate = moment.utc(updateProps.startDate || milestone.startDate).add(milestone.duration - 1, 'days').toDate().toISOString()
const endDate = moment.utc(updateProps.endDate || milestone.endDate).toDate().toISOString()
if (!_.isEqual(milestone.endDate, endDate)) {
updateProps.endDate = endDate
updateProps.updatedBy = updMilestone.updatedBy
Expand Down Expand Up @@ -176,7 +159,6 @@ function cascadeMilestones(originalMilestone, updatedMilestone, timelineMileston

// we need to recalculate change in fields because we update some fields before making actual update
const needToCascade = !_.isEqual(original.completionDate, updated.completionDate) // completion date changed
|| original.duration !== updated.duration // duration changed
|| original.actualStartDate !== updated.actualStartDate // actual start date updated

if (needToCascade) {
Expand All @@ -199,3 +181,8 @@ export const processUpdateMilestone = (milestone, updatedProps, timelineMileston

return { updatedMilestone, updatedTimelineMilestones }
}


export const processDeleteMilestone = (index, timelineMilestones) => {
return update(timelineMilestones, {$splice:[ [index, 1]]})
}
11 changes: 9 additions & 2 deletions src/projects/actions/productsTimelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {
PHASE_STATUS_COMPLETED,
BULK_UPDATE_PRODUCT_MILESTONES,
} from '../../config/constants'
import { processUpdateMilestone } from '../../helpers/milestoneHelper'
import { processUpdateMilestone,
processDeleteMilestone
} from '../../helpers/milestoneHelper'

/**
* Get the next milestone in the list, which is not hidden
Expand Down Expand Up @@ -110,7 +112,12 @@ export function updateProductMilestone(productId, timelineId, milestoneId, updat
const timeline = getState().productsTimelines[productId].timeline
const milestoneIdx = _.findIndex(timeline.milestones, { id: milestoneId })
const milestone = timeline.milestones[milestoneIdx]
const updatedTimelineMilestones = processUpdateMilestone(milestone, updatedProps, timeline.milestones).updatedTimelineMilestones
let updatedTimelineMilestones
if (!updatedProps) {
updatedTimelineMilestones = processDeleteMilestone(milestoneIdx, timeline.milestones)
} else {
updatedTimelineMilestones = processUpdateMilestone(milestone, updatedProps, timeline.milestones).updatedTimelineMilestones
}

dispatch({
type: UPDATE_PRODUCT_MILESTONE_PENDING,
Expand Down
47 changes: 41 additions & 6 deletions src/projects/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getProjectMemberInvites,
} from '../../api/projectMemberInvites'
import {
updateMilestones,
createTimeline,
} from '../../api/timelines'
import {
Expand Down Expand Up @@ -65,6 +66,7 @@ import {
PHASE_STATUS_DRAFT,
LOAD_PROJECT_MEMBERS,
LOAD_PROJECT_MEMBER_INVITES,
CREATE_PROJECT_PHASE_TIMELINE_MILESTONES,
LOAD_PROJECT_MEMBER
} from '../../config/constants'
import {
Expand Down Expand Up @@ -328,6 +330,41 @@ export function createProjectPhaseAndProduct(project, projectTemplate, status =
})
}


/**
* Create phase and product and milestones for the project
*
* @param {Object} project project
* @param {Object} projectTemplate project template
* @param {String} status (optional) project/phase status
* @param {Object} startDate phase startDate
* @param {Object} endDate phase endDate
* @param {Array} milestones milestones
*
* @return {Promise} project
*/
function createPhaseAndMilestonesRequest(project, projectTemplate, status = PHASE_STATUS_DRAFT, startDate, endDate, milestones) {
return createProjectPhaseAndProduct(project, projectTemplate, status, startDate, endDate).then(({timeline, phase, project, product}) => {
return updateMilestones(timeline.id, milestones).then((data) => ({
phase,
project,
product,
timeline,
milestones: data
}))
})
}


export function createPhaseAndMilestones(project, projectTemplate, status, startDate, endDate, milestones) {
return (dispatch) => {
return dispatch({
type: CREATE_PROJECT_PHASE_TIMELINE_MILESTONES,
payload: createPhaseAndMilestonesRequest(project, projectTemplate, status, startDate, endDate, milestones)
})
}
}

export function deleteProjectPhase(projectId, phaseId) {
return (dispatch) => {
return dispatch({
Expand Down Expand Up @@ -433,11 +470,9 @@ export function updatePhase(projectId, phaseId, updatedProps, phaseIndex) {
const phaseStartDate = timeline ? timeline.startDate : phase.startDate
const startDateChanged = updatedProps.startDate ? updatedProps.startDate.diff(phaseStartDate) : null
const phaseActivated = phaseStatusChanged && updatedProps.status === PHASE_STATUS_ACTIVE
if (phaseActivated) {
const duration = updatedProps.duration ? updatedProps.duration : phase.duration
updatedProps.startDate = moment().utc().hours(0).minutes(0).seconds(0).milliseconds(0).format('YYYY-MM-DD')
updatedProps.endDate = moment(updatedProps.startDate).add(duration - 1, 'days').format('YYYY-MM-DD')
}

updatedProps.startDate = moment(updatedProps.startDate).format('YYYY-MM-DD')
updatedProps.endDate = moment(updatedProps.endDate).format('YYYY-MM-DD')

return dispatch({
type: UPDATE_PHASE,
Expand Down Expand Up @@ -638,4 +673,4 @@ export function loadProjectMember(projectId, memberId) {
payload: getProjectMember(projectId, memberId)
})
}
}
}
Loading