diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss index d7361ebb5..6922dc988 100644 --- a/src/components/Layout/Layout.scss +++ b/src/components/Layout/Layout.scss @@ -22,8 +22,5 @@ &[data-route*='new-project'] { background-color: $tc-gray-neutral-light; } - &[data-route*='add-phase'] { - background-color: $tc-gray-neutral-light; - } } } diff --git a/src/components/TopBar/TopBarContainer.js b/src/components/TopBar/TopBarContainer.js index be38a5dcf..3c34174e4 100644 --- a/src/components/TopBar/TopBarContainer.js +++ b/src/components/TopBar/TopBarContainer.js @@ -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 ( -
- ) - } const { user, toolbar, userRoles } = this.props const userHandle = _.get(user, 'handle') const bigPhotoURL = _.get(user, 'photoURL') diff --git a/src/config/constants.js b/src/config/constants.js index 54a553bba..e7aef90d0 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -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' @@ -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', diff --git a/src/helpers/milestoneHelper.js b/src/helpers/milestoneHelper.js index 1ae729cbb..cd5351cd9 100644 --- a/src/helpers/milestoneHelper.js +++ b/src/helpers/milestoneHelper.js @@ -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) @@ -64,14 +64,11 @@ 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) { @@ -79,25 +76,11 @@ function updateMilestone(milestone, updatedProps) { // 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 } @@ -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 @@ -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) { @@ -199,3 +181,8 @@ export const processUpdateMilestone = (milestone, updatedProps, timelineMileston return { updatedMilestone, updatedTimelineMilestones } } + + +export const processDeleteMilestone = (index, timelineMilestones) => { + return update(timelineMilestones, {$splice:[ [index, 1]]}) +} diff --git a/src/projects/actions/productsTimelines.js b/src/projects/actions/productsTimelines.js index 25e44d253..e902d9f40 100644 --- a/src/projects/actions/productsTimelines.js +++ b/src/projects/actions/productsTimelines.js @@ -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 @@ -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, diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js index 8558583fd..2905435c3 100644 --- a/src/projects/actions/project.js +++ b/src/projects/actions/project.js @@ -19,6 +19,7 @@ import { getProjectMemberInvites, } from '../../api/projectMemberInvites' import { + updateMilestones, createTimeline, } from '../../api/timelines' import { @@ -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 { @@ -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({ @@ -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, @@ -638,4 +673,4 @@ export function loadProjectMember(projectId, memberId) { payload: getProjectMember(projectId, memberId) }) } -} \ No newline at end of file +} diff --git a/src/projects/detail/components/PhaseCard/EditStageForm.jsx b/src/projects/detail/components/PhaseCard/EditStageForm.jsx index 523dd90b2..615dfdec8 100644 --- a/src/projects/detail/components/PhaseCard/EditStageForm.jsx +++ b/src/projects/detail/components/PhaseCard/EditStageForm.jsx @@ -14,34 +14,24 @@ const TCFormFields = FormsyForm.Fields // import enhanceDropdown from 'appirio-tech-react-components/components/Dropdown/enhanceDropdown' import { updatePhase as updatePhaseAction, firePhaseDirty, firePhaseDirtyUndo } from '../../../actions/project' import LoadingIndicator from '../../../../components/LoadingIndicator/LoadingIndicator' -import SelectDropdown from '../../../../components/SelectDropdown/SelectDropdown' -import { PHASE_STATUS_COMPLETED, PHASE_STATUS, PHASE_STATUS_ACTIVE, PHASE_STATUS_DRAFT } from '../../../../config/constants' -import Tooltip from 'appirio-tech-react-components/components/Tooltip/Tooltip' -import { TOOLTIP_DEFAULT_DELAY } from '../../../../config/constants' -import { getPhaseActualData } from '../../../../helpers/projectHelper' +import { PHASE_STATUS_COMPLETED, PHASE_STATUS_ACTIVE, PHASE_STATUS_DRAFT } from '../../../../config/constants' import DeletePhase from './DeletePhase' import { hasPermission } from '../../../../helpers/permissions' import { PERMISSIONS } from '../../../../config/permissions' const moment = extendMoment(Moment) -const phaseStatuses = PHASE_STATUS.map(ps => ({ - title: ps.name, - value: ps.value, -})) class EditStageForm extends React.Component { constructor(props) { super(props) this.state = { + publishClicked: false, isUpdating: false, isEdittable: (_.get(props, 'phase.status') !== PHASE_STATUS_COMPLETED) || (_.get(props, 'canEditCompletedPhase')), - disableActiveStatusFields: _.get(props, 'phase.status') !== PHASE_STATUS_ACTIVE, showPhaseOverlapWarning: false, phaseIsdirty: false, showActivatingWarning: false, - // we have to control phase status separately, so we can restore its when we need - selectedPhaseStatus: _.get(props, 'phase.status') } this.submitValue = this.submitValue.bind(this) this.enableButton = this.enableButton.bind(this) @@ -52,7 +42,7 @@ class EditStageForm extends React.Component { this.showActivatingWarning = this.showActivatingWarning.bind(this) this.cancelActivatingPhase = this.cancelActivatingPhase.bind(this) this.onFormSubmit = this.onFormSubmit.bind(this) - this.updateSelectedPhaseStatus = this.updateSelectedPhaseStatus.bind(this) + this.onPublishClick = this.onPublishClick.bind(this) } showActivatingWarning() { @@ -62,23 +52,8 @@ class EditStageForm extends React.Component { } cancelActivatingPhase() { - const phaseStatus = _.get(this.props, 'phase.status') this.setState({ showActivatingWarning: false, - // to restore phase status first we change selected value to nothing - // and after will again put initial value, this will force SelectDropdown to change - // to initial value - selectedPhaseStatus: '', - }, () => { - this.setState({ - selectedPhaseStatus: phaseStatus, - }) - }) - } - - updateSelectedPhaseStatus(selectedOption) { - this.setState({ - selectedPhaseStatus: selectedOption.value, }) } @@ -86,15 +61,7 @@ class EditStageForm extends React.Component { this.setState({ isUpdating: nextProps.isUpdating, isEdittable: nextProps.phase.status !== PHASE_STATUS_COMPLETED || nextProps.canEditCompletedPhase, - disableActiveStatusFields: nextProps.phase.status !== PHASE_STATUS_ACTIVE, }) - - // update selected phase status if it was updated at the props - const prevPhaseStatus = _.get(this.props, 'phase.status') - const nextPhaseStatus = _.get(nextProps, 'phase.status') - if (nextPhaseStatus && prevPhaseStatus !== nextPhaseStatus) { - this.setState({ selectedPhaseStatus: nextPhaseStatus }) - } } componentDidMount() { @@ -108,26 +75,35 @@ class EditStageForm extends React.Component { submitValue(model) { const { phase, phaseIndex, updatePhaseAction } = this.props + const { + publishClicked + } = this.state const updatedStartDate = moment.utc(new Date(model.startDate)) - const duration = model.duration ? model.duration : 1 - const endDate = model.status === PHASE_STATUS_COMPLETED ? moment.utc(new Date()) : moment.utc(updatedStartDate).add(duration - 1, 'days') + const updatedEndDate = model.status === PHASE_STATUS_COMPLETED ? moment.utc(new Date()) : moment.utc(model.endDate) + let newStatus = phase.status + if (publishClicked && phase.status === PHASE_STATUS_DRAFT) { + newStatus = PHASE_STATUS_ACTIVE + } const updateParam = _.assign({}, model, { startDate: updatedStartDate, - endDate: endDate || '', - duration + endDate: updatedEndDate || '', + status: newStatus + }) + this.setState({ + isUpdating: true, + publishClicked: false }) - this.setState({isUpdating: true}) updatePhaseAction(phase.projectId, phase.id, updateParam, phaseIndex) } onFormSubmit(model) { const { phase } = this.props - const { showActivatingWarning } = this.state + const { showActivatingWarning, publishClicked } = this.state if ( !showActivatingWarning && - phase.status !== PHASE_STATUS_ACTIVE && - model.status === PHASE_STATUS_ACTIVE + publishClicked && + phase.status === PHASE_STATUS_DRAFT ) { this.showActivatingWarning() } else { @@ -166,6 +142,14 @@ class EditStageForm extends React.Component { this.props.cancel() } + /** + * when in draft status, click publish button + */ + onPublishClick() { + this.setState({ + publishClicked: true, + }) + } /** * Handles the change event of the form. * @@ -175,10 +159,12 @@ class EditStageForm extends React.Component { handleChange(change) { const { phases, phase, phaseIndex } = this.props let showPhaseOverlapWarning = false - // if start date's day or duration is updated for a phase, we need to update other phases dates accordingly - const phaseDay = moment.utc(new Date(phase.startDate)).format('DD') - const changedDay = moment.utc(new Date(change.startDate)).format('DD') - if (phaseDay !== changedDay || phase.duration !== change.duration) { + // if start date's start day or end day is updated for a phase, we need to update other phases dates accordingly + const phaseStartDay = moment.utc(new Date(phase.startDate)).format('YYYY-MM-DD') + const changedStartDay = moment.utc(new Date(change.startDate)).format('YYYY-MM-DD') + const phaseEndDay = moment.utc(new Date(phase.endDate)).format('YYYY-MM-DD') + const changedEndDay = moment.utc(new Date(change.endDate)).format('YYYY-MM-DD') + if (phaseStartDay !== changedStartDay || phaseEndDay !== changedEndDay) { // console.log('Need to sync phases') const reqChanges = this.checkOverlappingPhases(phases, phase, phaseIndex, change) //console.log('reqChanges : ', reqChanges) @@ -191,7 +177,6 @@ class EditStageForm extends React.Component { // console.log('No need to sync phases') } this.setState({ - disableActiveStatusFields: change.status !== PHASE_STATUS_ACTIVE, showPhaseOverlapWarning }) if (this.isChanged()) { @@ -206,8 +191,8 @@ class EditStageForm extends React.Component { } checkOverlappingPhases(phases, refPhase, refPhaseIndex, updatedPhase) { - // if startDate or duration is not set in the current update, we don't need to do anything - if (!updatedPhase.startDate || !updatedPhase.duration) return false + // if startDate or endDate is not set in the current update, we don't need to do anything + if (!updatedPhase.startDate || !updatedPhase.endDate) return false //Possible mutations //!date,!duration //both changed //!date,duration //date changed @@ -215,13 +200,13 @@ class EditStageForm extends React.Component { const phasesToBeUpdated = [] let overLapping = false const updatedPhaseStartDate = updatedPhase ? moment(new Date(updatedPhase.startDate)) : null - const updatedPhaseEndDate = updatedPhase ? moment(new Date(updatedPhase.startDate)).add(updatedPhase.duration - 1, 'days') : null + const updatedPhaseEndDate = updatedPhase ? moment(new Date(updatedPhase.endDate)) : null const updatedPhaseRange = moment().range(updatedPhaseStartDate, updatedPhaseEndDate) for(let i =0; i < phases.length; i++) { overLapping = false if(i !== refPhaseIndex) { const currentStartDate = moment(new Date(phases[i].startDate)) - const currentEndDate = moment(new Date(phases[i].startDate)).add(phases[i].duration - 1, 'days') + const currentEndDate = moment(new Date(phases[i].endDate)) const currentPhaseRange = moment().range(currentStartDate, currentEndDate) if(currentPhaseRange.contains(updatedPhaseStartDate)) { overLapping = true @@ -246,20 +231,14 @@ class EditStageForm extends React.Component { } render() { - const { phase, isUpdating, timeline, deleteProjectPhase } = this.props - const { isEdittable, showPhaseOverlapWarning, showActivatingWarning, selectedPhaseStatus } = this.state + const { phase, isUpdating, deleteProjectPhase } = this.props + const { isEdittable, showPhaseOverlapWarning, showActivatingWarning } = this.state let startDate = phase.startDate ? new Date(phase.startDate) : new Date() startDate = moment.utc(startDate).format('YYYY-MM-DD') - const hasTimeline = !!timeline + let endDate = phase.endDate ? new Date(phase.endDate) : new Date() + endDate = moment.utc(endDate).format('YYYY-MM-DD') const canDelete = phase.status !== PHASE_STATUS_ACTIVE && phase.status !== PHASE_STATUS_COMPLETED - // don't allow to selected completed status if product has timeline - const activePhaseStatuses = phaseStatuses.map((status) => ({ - ...status, - disabled: hasTimeline && status.value === PHASE_STATUS_COMPLETED, - toolTipMessage: (hasTimeline && status.value === PHASE_STATUS_COMPLETED) ? 'Once activated, phase delivery is controlled by the milestones.': null, - })) - - const { progress, duration } = getPhaseActualData(phase, timeline) + const isDraft = phase.status === PHASE_STATUS_DRAFT return (