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 (
@@ -282,71 +261,39 @@ class EditStageForm extends React.Component { Warning: You are about to manually change the start/end date of this phase, Please ensure the start and end dates of all subsequent phases (where applicable) are updated in line with this change.
}
- -
-
-
- - -
-
- {hasTimeline && phase.status === PHASE_STATUS_ACTIVE ? ( - -
-
- - -
-
-
- Phase status is controlled by statuses of individual milestones -
-
- ) : ( -
- - -
- )} - +
{!showActivatingWarning ? ( @@ -354,7 +301,14 @@ class EditStageForm extends React.Component { + >Save Changes + {isDraft ? ( + + ) : null}
) : (
diff --git a/src/projects/detail/components/PhaseCard/EditStageForm.scss b/src/projects/detail/components/PhaseCard/EditStageForm.scss index 658df6495..c6cb205e8 100644 --- a/src/projects/detail/components/PhaseCard/EditStageForm.scss +++ b/src/projects/detail/components/PhaseCard/EditStageForm.scss @@ -35,14 +35,26 @@ display: inline-block; width: 50%; - :global .dropdown-wrap { - width: auto; - margin-left: 0px; + :global { + + .dropdown-wrap { + width: auto; + margin-left: 0px; + } + + .error-message { + width:calc(100% - 170px)!important; + margin: -5px 0 10px; + float: right; + } + } input { display: inline-block; width:calc(100% - 170px)!important; } + + label { float: left; margin: 0px 10px 10px; @@ -149,6 +161,7 @@ float: none; .input-row { width: 100%; + } } .label-layer, @@ -163,12 +176,19 @@ position: relative; top: 0; } - :global .dropdown-wrap { - width: 100%; - margin-left: 0px; - height: 40px; - div { - padding: 0 5px!important; + :global { + .dropdown-wrap { + width: 100%; + margin-left: 0px; + height: 40px; + div { + padding: 0 5px!important; + } + } + + .error-message { + width: 100% !important; + float: initial; } } div { @@ -204,4 +224,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/projects/detail/components/timeline/Milestone/Milestone.jsx b/src/projects/detail/components/timeline/Milestone/Milestone.jsx index e92e9cd0c..4e965b364 100644 --- a/src/projects/detail/components/timeline/Milestone/Milestone.jsx +++ b/src/projects/detail/components/timeline/Milestone/Milestone.jsx @@ -25,6 +25,7 @@ import DotIndicator from '../DotIndicator' import MobilePage from '../../../../../components/MobilePage/MobilePage' import MediaQuery from 'react-responsive' import XMartIcon from '../../../../../assets/icons/x-mark.svg' +import TrashIcon from '../../../../../assets/icons/icon-trash.svg' import { MILESTONE_STATUS, SCREEN_BREAKPOINT_MD } from '../../../../../config/constants' @@ -32,11 +33,27 @@ import { PERMISSIONS } from '../../../../../config/permissions' import {hasPermission} from '../../../../../helpers/permissions' import './Milestone.scss' + +const TYPE_OPTIONS = [ + { + title: 'Reporting', + value: 'generic-work', + }, + { + title: 'Deliverable Review', + value: 'add-links', + }, + { + title: 'Final Deliverable Review', + value: 'delivery-dev', + }, +] + class Milestone extends React.Component { constructor(props) { super(props) - this.deletePost = this.deletePost.bind(this) + this.onDeleteClick = this.onDeleteClick.bind(this) this.hoverHeader = this.hoverHeader.bind(this) this.unHoverHeader = this.unHoverHeader.bind(this) this.toggleEditLink = this.toggleEditLink.bind(this) @@ -118,9 +135,15 @@ class Milestone extends React.Component { updateMilestoneWithData(values) { const { milestone, updateMilestone } = this.props + if (values.type === 'Deprecated type') { + values.type = milestone.type + } + const milestoneData = { ...values } + milestoneData.startDate = moment.utc(new Date(values.startDate)) + milestoneData.endDate = moment.utc(new Date(values.endDate)) if (values.actualStartDate) { milestoneData.actualStartDate = moment.utc(new Date(values.actualStartDate)) } @@ -190,6 +213,7 @@ class Milestone extends React.Component { } completeMilestone(milestone.id, updatedProps) } + completeFinalFixesMilestone(updatedProps = {}) { const { completeFinalFixesMilestone, milestone } = this.props @@ -216,14 +240,50 @@ class Milestone extends React.Component { submitFinalFixesRequest(milestone.id, finalFixRequests) } + onDeleteClick() { + const { milestone, updateMilestone } = this.props + + if (confirm(`Are you sure you want to delete milestone '${milestone.name}'?`)) { + updateMilestone(milestone.id, null) + } + } + + getSelectOptions() { + const { + milestone, + } = this.props + const option = _.find(TYPE_OPTIONS, (o) => o.value === milestone.type) + const options = _.clone(TYPE_OPTIONS) + if (!option) { + options.push( + { + title: 'Deprecated type', + value: milestone.type + } + ) + + } + return options + } + + getSelectLabel(type) { + const option = _.find(TYPE_OPTIONS, (o) => o.value === type) + if (!option) { + return 'Deprecated type' + } + return option.title + } + + render() { const { milestone, + index, currentUser, previousMilestone, } = this.props const { isEditing, isMobileEditing } = this.state - + const disabledType = milestone.status !== MILESTONE_STATUS.DRAFT const isPlanned = milestone.status === MILESTONE_STATUS.PLANNED const isActive = milestone.status === MILESTONE_STATUS.ACTIVE const isCompleted = milestone.status === MILESTONE_STATUS.COMPLETED @@ -234,94 +294,137 @@ class Milestone extends React.Component { const isUpdating = milestone.isUpdating const isActualDateEditable = this.isActualStartDateEditable() const isCompletionDateEditable = this.isCompletionDateEditable() + + const hideDelete = index === 0 && milestone.type === 'generic-work' + const editForm = (
+
+ {hideDelete ? null: } {editForm}
)} @@ -502,6 +606,7 @@ Milestone.propTypes = { currentUser: PT.object.isRequired, extendMilestone: PT.func.isRequired, milestone: PT.object.isRequired, + index: PT.number.isRequired, submitFinalFixesRequest: PT.func.isRequired, updateMilestone: PT.func.isRequired, } diff --git a/src/projects/detail/components/timeline/Milestone/Milestone.scss b/src/projects/detail/components/timeline/Milestone/Milestone.scss index 620a3e33d..d02fe6089 100644 --- a/src/projects/detail/components/timeline/Milestone/Milestone.scss +++ b/src/projects/detail/components/timeline/Milestone/Milestone.scss @@ -10,7 +10,13 @@ height: auto; } + :global(.error + p) { + position: relative !important; + } + :global(.dropdown-wrap) { + margin-bottom: 10px; + } @include roboto; a { @@ -306,4 +312,15 @@ form { flex: 1; } -} \ No newline at end of file +} + +.edit-form { + position: relative; + >i { + position: absolute; + right: 25px; + top: 20px; + cursor: pointer; + + } +} diff --git a/src/projects/detail/components/timeline/Timeline/Timeline.jsx b/src/projects/detail/components/timeline/Timeline/Timeline.jsx index 12ed9cbb8..875e1ec29 100644 --- a/src/projects/detail/components/timeline/Timeline/Timeline.jsx +++ b/src/projects/detail/components/timeline/Timeline/Timeline.jsx @@ -113,10 +113,11 @@ class Timeline extends React.Component { id={`phase-${phaseId}-timeline-${timeline.id}`} criteria={buildPhaseTimelineNotificationsCriteria(timeline)} /> - {_.reject(orderedMilestones, { hidden: true }).map((milestone) => ( + {_.reject(orderedMilestones, { hidden: true }).map((milestone, index) => ( { + const arrs = k.match(/(\w+)_(\d+)/) + const arrIndex = arrs[2] + const objKey = arrs[1] === 'title'? 'name': arrs[1] + if (!milestones[arrIndex]) { + milestones[arrIndex] = {} + } + milestones[arrIndex][objKey] = model[k] + }) + + _.forEach(milestones, (m) => { + m.status = 'reviewed' + // TODO add mock data + m.duration = 1 + m.order = 1 + m.hidden =false + m.completedText = 'completed text' + m.activeText = 'active text' + m.description = 'description' + m.plannedText ='planned text' + m.details = {} + m.blockedText = 'blocked text' + }) + + const projectTemplate = { + name: phaseData.title, + id:166, + } + if (publishClicked) { + createPhaseAndMilestones(project, projectTemplate, 'active', phaseData.startDate, phaseData.endDate, milestones) + } else { + createPhaseAndMilestones(project, projectTemplate, 'draft', phaseData.startDate, phaseData.endDate, milestones) + } + this.setState({ + publishClicked: false, + isAddButtonClicked: false + }) + } + + isChanged() { + // We check if this.refs.form exists because this may be called before the + // first render, in which case it will be undefined. + return (this.refs.form && this.refs.form.isChanged()) + } + /** + * Handles the change event of the form. + * + * @param change changed form model in flattened form + * @param isChanged flag that indicates if form actually changed from initial model values + */ + handleChange(change) { + const { + milestones + } = this.state + + + // omit phase fields + _.forEach(_.keys(_.omit(change, ['title', 'startDate', 'endDate'])), (k) => { + const arrs = k.match(/(\w+)_(\d+)/) + const arrIndex = arrs[2] + const objKey = arrs[1] + if(change[k] && change[k] !== milestones[arrIndex][objKey]) { + milestones[arrIndex][objKey] = change[k] + // set default title with option type + if (objKey === 'type' && !milestones[arrIndex]['title']) { + milestones[arrIndex]['title'] = this.getOptionType(change[k]) + } + } + }) + + + this.setState({milestones}) + } + + getOptionType(val) { + return _.find(TYPE_OPTIONS, (v) => v.value === val).title + } + + onDeleteMilestoneClick(index) { + const { + milestones + } = this.state + milestones.splice(index, 1) + this.setState({ + milestones + }) + } + + onAddMilestoneClick() { + const { + milestones + } = this.state + + const defaultData = { + startDate: moment(_.last(milestones).endDate).format('YYYY-MM-DD'), + endDate: moment(_.last(milestones).endDate).add(3, 'days').format('YYYY-MM-DD') + } + milestones.push(defaultData) + + this.setState({ + addMilestonesButtonClicked: true, + milestones + }) + } + + renderMilestones() { + + const { + isCreatingPhase + } = this.props + const { + milestones + } = this.state + + const ms = _.map(milestones, (m, index) => { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+ this.onDeleteMilestoneClick(index)} title="trash"> +
+ ) + }) + return ( +
+ {ms} +
+ +
+
+ ) + + } + renderTab() { + const tabs = [ + { + onClick: () => {}, + label: 'Timeline', + isActive: true, + hasNotifications: false, + }] + return ( +
+ +
+ ) + } + renderAddingForm() { + const { + isCreatingPhase + } = this.props + return ( +
+ +
+
+ +
+
+ + +
+ {this.renderTab()} + {this.renderMilestones()} +
+ + + +
+
+
+
+ ) + + } render() { const { project, phases, phasesNonDirty, + isCreatingPhase, isLoadingPhases, notifications, productTemplates, @@ -135,6 +493,9 @@ class DashboardContainer extends React.Component { location, estimationQuestion, } = this.props + const { + isAddButtonClicked + } = this.state const projectTemplate = project && project.templateId && projectTemplates ? (getProjectTemplateById(projectTemplates, project.templateId)) : null let template @@ -262,9 +623,16 @@ class DashboardContainer extends React.Component { ) : ( )} - {isProjectLive && hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && !isLoadingPhases && (
- Add New Phase + {isCreatingPhase? : null} + + {!isAddButtonClicked && isProjectLive && !isCreatingPhase && hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && !isLoadingPhases && (
+
)} + {!isCreatingPhase && isAddButtonClicked? this.renderAddingForm(): null}
)} @@ -281,6 +649,7 @@ const mapStateToProps = ({ notifications, projectState, projectTopics, templates } return { + isCreatingPhase: projectState.isCreatingPhase, notifications: notifications.notifications, productTemplates: templates.productTemplates, projectTemplates: templates.projectTemplates, @@ -300,6 +669,7 @@ const mapDispatchToProps = { toggleNotificationRead, toggleBundledNotificationRead, updateProduct, + createPhaseAndMilestones, fireProductDirty, fireProductDirtyUndo, addProductAttachment, diff --git a/src/projects/detail/containers/DashboardContainer.scss b/src/projects/detail/containers/DashboardContainer.scss index 4891e7ec5..79f795dd8 100644 --- a/src/projects/detail/containers/DashboardContainer.scss +++ b/src/projects/detail/containers/DashboardContainer.scss @@ -1,4 +1,289 @@ +@import '../../../styles/includes'; +@import '~tc-ui/src/styles/tc-includes'; + .add-button-container { text-align: center; margin-top: 10px; -} \ No newline at end of file +} + + +.form { + :global .error-message { + color: $tc-orange-70; + border: 1px solid $tc-orange-30; + } +} + +.label-layer:nth-child(4) > :global(.Tooltip) { + width: 50%; + float: left; + .input-row { + width: 100%; + } +} + + +.input-row { + display: inline-block; + width: 50%; + + :global { + + .dropdown-wrap { + width: auto; + margin-left: 0px; + } + + .error-message { + width:calc(100% - 170px)!important; + margin: -5px 0 10px; + float: right; + } + + } + input { + display: inline-block; + width:calc(100% - 170px)!important; + } + + + label { + float: left; + margin: 0px 10px 10px; + text-align: right; + height: 40px; + line-height: 40px; + white-space: nowrap; + display: block; + width: 150px; + + } + + input:not([type="checkbox"]):disabled { + padding: 0; + } + :global(.SelectDropdown:not(.dropdown-wrap)) { + position: relative; + top: 12px; + } + + .input { + &::-webkit-input-placeholder { + text-transform: initial; + color:$tc-gray-30; + font-size: 15px; + font-weight: 400; + } + &:-moz-placeholder { + text-transform: initial; + color:$tc-gray-30; + font-size: 15px; + font-weight: 400; + } + &::-moz-placeholder { + text-transform: initial; + color:$tc-gray-30; + font-size: 15px; + font-weight: 400; + } + &:-ms-input-placeholder { + text-transform: initial; + color:$tc-gray-30; + font-size: 15px; + font-weight: 400; + } + } +} + +.title-label-layer { + + .input-row { + width: 100%; + input { + display: inline-block; + width:calc(100% - 170px); + } + label { + display: inline-block; + width: 150px; + } + } +} + +.group-bottom { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; + margin-top: 20px; + padding-bottom: 20px; + + button { + margin: 0 5px 0 5px; + &.disable { + opacity: 0.5; + } + } +} + +.message { + background-color: $tc-red-10; + padding: 4 * $base-unit 4 * $base-unit 0 4 * $base-unit; + text-align: center; + + .message-title { + @include roboto-bold; + color: $tc-red-100; + font-size: 15px; + } + + .message-text { + @include roboto; + font-size: 13px; + line-height: 20px; + margin-top: 2 * $base-unit; + } +} + +// screen < 1024px +@media screen and (max-width: 1023px) { + .label-layer:nth-child(4) > :global(.Tooltip) { + width: 100%; + float: none; + .input-row { + width: 100%; + + } + } + .label-layer, + .input-row { + display: block; + width: 100%; + input:not([type="checkbox"]):disabled { + width: 100%; + } + :global(.SelectDropdown:not(.dropdown-wrap)) { + width: 100%; + position: relative; + top: 0; + } + :global { + .dropdown-wrap { + width: 100%; + margin-left: 0px; + height: 40px; + div { + padding: 0 5px!important; + } + } + + .error-message { + width: 100% !important; + float: initial; + } + } + div { + padding: 0!important; + } + input { + width: 100%!important; + display: block; + } + label { + width: 100%; + display: block; + text-align: left; + margin: 0; + float: none; + } + } + .title-label-layer { + display: block; + .input-row { + width: 100%; + display: block; + input { + width: 100%!important; + display: block; + } + label { + float: none; + width: 100%; + display: block; + margin: 0; + text-align: left; + } + } + } +} + +.tab-container { + border-top: 1px solid $tc-gray-10; + border-bottom: 1px solid $tc-gray-10; +} + +.milestone-item { + position: relative; + width: 90%; + + > i { + position: absolute; + top: 15px; + right: -60px; + cursor: pointer; + } +} + +.add-milestone-wrapper { + text-align: center; + padding: 10px 0 30px; + border-bottom: 1px solid #aaaaab; +} + +.add-phase-form { + margin-top: 30px; +} + +.add-milestone-form { + margin-top: 20px; + + .input-row { + width: 100%; + } + + > div > div:first-child { + margin-bottom: 10px; + > div { + display: initial; + >div:first-child { + float: left; + margin: 0px 10px 10px; + text-align: right; + height: 40px; + line-height: 40px; + white-space: nowrap; + display: block; + width: 150px; + + @media screen and (max-width: 1023px) { + margin: initial; + > div { + text-align:left; + } + } + } + >div:last-child { + display: inline-block; + width: calc(100% - 170px) !important; + + @media screen and (max-width: 1023px) { + width: 100% !important; + } + } + + } + + + } +} diff --git a/src/projects/detail/containers/ProjectAddPhaseContainer.jsx b/src/projects/detail/containers/ProjectAddPhaseContainer.jsx deleted file mode 100644 index 804d3ffc9..000000000 --- a/src/projects/detail/containers/ProjectAddPhaseContainer.jsx +++ /dev/null @@ -1,158 +0,0 @@ -/** - * ProjectAddPhaseContainer container - * displays content of the Project Add Phase tab - * - * NOTE data is loaded by the parent ProjectDetail component - */ -import _ from 'lodash' -import React from 'react' -import PropTypes from 'prop-types' -import qs from 'query-string' -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { renderComponent, branch, compose, withProps } from 'recompose' -import spinnerWhileLoading from '../../../components/LoadingSpinner' -import { createProduct } from '../../actions/project' -import { getProductTemplateByKey } from '../../../helpers/templates' - -import CoderBot from '../../../components/CoderBot/CoderBot' -import Wizard from '../../../components/Wizard' -import SelectProductTemplate from '../../create/components/SelectProductTemplate' -import { - CREATE_PROJECT_FAILURE, -} from '../../../config/constants' - -import '../../../projects/create/components/ProjectWizard.scss' -import styles from './ProjectAddPhaseContainer.scss' - -const page404 = compose( - withProps({code:500}) -) -// this handles showing error page when there is an error in loading the page -const showCoderBotIfError = (hasError) => - branch( - hasError, - renderComponent(page404(CoderBot)), - t => t - ) -const errorHandler = showCoderBotIfError(props => props.error && props.error.type === CREATE_PROJECT_FAILURE) - - -// This handles showing a spinner while the state is being loaded async -const spinner = spinnerWhileLoading(props => - !props.processing && !props.addingState && !props.isLoadingPhases -) - -const enhance = compose(errorHandler, spinner) - - -const CreateView = (props) => { - return ( -
- {} } - step={1} - shouldRenderBackButton={ (step) => step > 1 } - > -
- - -
- ) -} -const EnhancedCreateView = enhance(CreateView) - -class ProjectAddPhaseContainer extends React.Component { - constructor(props) { - super(props) - this.closeWizard = this.closeWizard.bind(this) - this.updateProductTemplate = this.updateProductTemplate.bind(this) - this.state = { - isChosenProduct: false, - shouldReloadPhases: false - } - } - - closeWizard() { - const { userRoles, location, project } = this.props - const isLoggedIn = userRoles && userRoles.length > 0 - const returnUrl = _.get(qs.parse(location.search), 'returnUrl', null) - if (returnUrl) { - window.location = returnUrl - } else { - if (isLoggedIn) { - this.props.history.push(`/projects/${project.id}`) - } else { - this.props.history.push('/') - } - } - } - - componentWillReceiveProps(props) { - const project = _.get(props, 'project', null) - if (!props.processing && !props.error && project && this.state.isChosenProduct) { - this.closeWizard() - } - } - - updateProductTemplate(projectTemplateKey) { - const props = this.props - const productTemplate = getProductTemplateByKey(props.allProductTemplates, projectTemplateKey) - if (productTemplate) { - props.createProduct(props.project, productTemplate, props.phases, props.timelines) - this.setState({isChosenProduct: true, shouldReloadPhases: true}) - } - } - - render() { - const props = this.props - const nonAddOnProductTemplates = _.filter(props.allProductTemplates, pt => !pt.isAddOn) - - return ( - - ) - } -} - -ProjectAddPhaseContainer.propTypes = { - userRoles: PropTypes.arrayOf(PropTypes.string).isRequired, - addingState: PropTypes.bool, - allProductTemplates: PropTypes.array, - productCategories: PropTypes.array -} - -ProjectAddPhaseContainer.defaultProps = { - userRoles: [], - addingState: false, - allProductTemplates: [], - productCategories: [] -} - -const mapStateToProps = ({projectState, loadUser, templates, productsTimelines }) => ({ - userRoles: _.get(loadUser, 'user.roles', []), - processing: projectState.processing, - error: projectState.error, - project: projectState.project, - isLoadingPhases: projectState.isLoadingPhases, - phases: projectState.phases, - templates, - timelines: productsTimelines, - productCategories: templates.productCategories, -}) - -const actionCreators = {createProduct} - -export default withRouter(connect(mapStateToProps, actionCreators)(ProjectAddPhaseContainer)) diff --git a/src/projects/detail/containers/ProjectAddPhaseContainer.scss b/src/projects/detail/containers/ProjectAddPhaseContainer.scss deleted file mode 100644 index 29c01dc9a..000000000 --- a/src/projects/detail/containers/ProjectAddPhaseContainer.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import '~tc-ui/src/styles/tc-includes'; - -.container { - // as block in this component comes inside Layout component - // we have to additionally remove padding at the top added by Layout - // to make Wizard properly fullscreen - margin-top: -60px; - - @media screen and (max-width: $screen-md - 1px) { - margin-top: 0; - } -} diff --git a/src/projects/reducers/productsTimelines.js b/src/projects/reducers/productsTimelines.js index 53820f0fb..279fd2c6b 100644 --- a/src/projects/reducers/productsTimelines.js +++ b/src/projects/reducers/productsTimelines.js @@ -6,6 +6,7 @@ import { LOAD_PRODUCT_TIMELINE_WITH_MILESTONES_PENDING, LOAD_PRODUCT_TIMELINE_WITH_MILESTONES_SUCCESS, LOAD_PRODUCT_TIMELINE_WITH_MILESTONES_FAILURE, + CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, UPDATE_PRODUCT_MILESTONE_PENDING, UPDATE_PRODUCT_MILESTONE_SUCCESS, UPDATE_PRODUCT_MILESTONE_FAILURE, @@ -129,6 +130,21 @@ export const productsTimelines = (state=initialState, action) => { const { type, payload, meta } = action switch (type) { + case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS: { + const {timeline, milestones} = action.payload + + timeline.milestones = milestones + + return update(state, { + [timeline.referenceId]: { + $set: { + isLoading: false, + timeline, + error: false + } + } + }) + } case LOAD_PRODUCT_TIMELINE_WITH_MILESTONES_PENDING: // if already have previously loaded timeline, just update some props diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index 7b57e87b0..5ce18f2a1 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -1,5 +1,6 @@ import { - LOAD_PROJECT_PENDING, LOAD_PROJECT_SUCCESS, LOAD_PROJECT_MEMBER_INVITE_PENDING, LOAD_PROJECT_MEMBER_INVITE_FAILURE, LOAD_PROJECT_MEMBER_INVITE_SUCCESS, LOAD_PROJECT_FAILURE, + CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE, + LOAD_PROJECT_PENDING, LOAD_PROJECT_SUCCESS, LOAD_PROJECT_MEMBER_INVITE_PENDING, LOAD_PROJECT_MEMBER_INVITE_SUCCESS, LOAD_PROJECT_FAILURE, CREATE_PROJECT_PENDING, CREATE_PROJECT_SUCCESS, CREATE_PROJECT_FAILURE, CREATE_PROJECT_STAGE_PENDING, CREATE_PROJECT_STAGE_SUCCESS, CREATE_PROJECT_STAGE_FAILURE, CLEAR_LOADED_PROJECT, UPDATE_PROJECT_PENDING, UPDATE_PROJECT_SUCCESS, UPDATE_PROJECT_FAILURE, DELETE_PROJECT_PENDING, DELETE_PROJECT_SUCCESS, DELETE_PROJECT_FAILURE, @@ -145,6 +146,20 @@ function getProductInPhases(phases, phaseId, productId) { export const projectState = function (state=initialState, action) { switch (action.type) { + case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING: + return Object.assign({}, state, { + isCreatingPhase: true + }) + case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS: { + const { phase, product } = action.payload + phase.products = [product] + return update(state, { + phases: { $push: [phase] }, + phasesNonDirty: { $push: [_.cloneDeep(phase)] }, + isCreatingPhase: { $set: false }, + }) + } + case EXPAND_PROJECT_PHASE: { const { phaseId, tab } = action.payload const currentPhaseTab = state.phasesStates[phaseId] || {} @@ -889,8 +904,9 @@ export const projectState = function (state=initialState, action) { case ADD_PRODUCT_ATTACHMENT_FAILURE: case REMOVE_PRODUCT_ATTACHMENT_FAILURE: case DELETE_PROJECT_PHASE_FAILURE: - case LOAD_PROJECT_MEMBER_INVITE_FAILURE: + case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE: return Object.assign({}, state, { + isCreatingPhase: false, isLoading: false, processing: false, processingMembers: false, diff --git a/src/projects/routes.jsx b/src/projects/routes.jsx index 4f497359a..b0d739676 100644 --- a/src/projects/routes.jsx +++ b/src/projects/routes.jsx @@ -12,7 +12,6 @@ import MessagesTabContainer from './detail/containers/MessagesTabContainer' import Scope from './detail/containers/ScopeContainer' import ProjectSummaryReport from './detail/containers/ProjectSummaryReportContainer' import AssetsLibrary from './detail/containers/AssetsLibraryContainer' -import ProjectAddPhaseContainer from './detail/containers/ProjectAddPhaseContainer' import ProjectMessages from './detail/Messages' import CoderBot from '../components/CoderBot/CoderBot' import SpecificationContainer from './detail/containers/SpecificationContainer' @@ -57,7 +56,6 @@ const ProjectDetailWithAuth = requiresAuthentication(() => /> } /> } /> - } /> } /> } /> } /> diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index fa1c46267..72affa32e 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -2,6 +2,8 @@ import _ from 'lodash' import Alert from 'react-s-alert' /* eslint-disable no-unused-vars */ import { + // bulk phase and milestones + CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, // Project CREATE_PROJECT_STAGE_SUCCESS, CREATE_PROJECT_SUCCESS, CREATE_PROJECT_FAILURE, @@ -80,6 +82,12 @@ export default function(state = {}, action) { return state } + case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS: { + //delay time for reload stage list of project after creating state + setTimeout(() => { Alert.success('Added New Phase To Project') }, 2000) + + return state + } case CREATE_PROJECT_STAGE_SUCCESS: { //delay time for reload stage list of project after creating state