diff --git a/src/projects/actions/productsTimelines.js b/src/projects/actions/productsTimelines.js index 25e44d253..935ed8e80 100644 --- a/src/projects/actions/productsTimelines.js +++ b/src/projects/actions/productsTimelines.js @@ -29,6 +29,7 @@ import { BULK_UPDATE_PRODUCT_MILESTONES, } from '../../config/constants' import { processUpdateMilestone } from '../../helpers/milestoneHelper' +import moment from 'moment' /** * Get the next milestone in the list, which is not hidden @@ -448,3 +449,79 @@ export function completeFinalFixesMilestone(productId, timelineId, milestoneId, }) } } + +/** + * Mark the milestone as 'completed' and append a 'deliverable-final-fixes' milestone after it + * @param {Number} productId product id + * @param {Number} timelineId timeline id + * @param {Number} milestoneId milestone id + * @param {String} finalFixRequest final fixes request text + */ +export function submitDeliverableFinalFixesRequest(productId, timelineId, milestoneId, finalFixRequest) { + return (dispatch, getState) => { + const state = getState() + const timeline = state.productsTimelines[productId].timeline + const milestoneIdx = _.findIndex(timeline.milestones, { id: milestoneId }) + const milestone = timeline.milestones[milestoneIdx] + + let updatedTimelineMilestones = [ + ...timeline.milestones, + ] + + updatedTimelineMilestones = processUpdateMilestone( + milestone, { + status: MILESTONE_STATUS.COMPLETED, + }, updatedTimelineMilestones + ).updatedTimelineMilestones + + const finalFixesMilestone = { + type: 'deliverable-final-fixes', + startDate: milestone.endDate, + endDate: moment(milestone.endDate).add(3, 'day').toISOString(), + status: MILESTONE_STATUS.ACTIVE, + details: { + content: { + finalFixesRequest: finalFixRequest, + } + }, + name: 'Final Fixes', + duration: 3, + order: timeline.milestones.length + 1, + hidden: false, + completedText: 'completed text', + activeText: 'active text', + description: 'description', + plannedText: 'planned text', + blockedText: 'blocked text', + } + + updatedTimelineMilestones.splice(milestoneIdx + 1, 0, finalFixesMilestone) + + dispatch({ + type: SUBMIT_FINAL_FIXES_REQUEST_PENDING, + meta: { productId, milestoneId } + }) + + const milestones = updatedTimelineMilestones.map(milestone => _.omit(milestone, ['timelineId', 'error', 'isUpdating', 'statusHistory'])) + + return dispatch({ + type: BULK_UPDATE_PRODUCT_MILESTONES, + payload: updateMilestones(timelineId, milestones), + meta: { + productId, + } + }).then(() => { + dispatch({ + type: SUBMIT_FINAL_FIXES_REQUEST_SUCCESS, + meta: { productId, milestoneId } + }) + }).catch((error) => { + dispatch({ + type: SUBMIT_FINAL_FIXES_REQUEST_FAILURE, + payload: error, + meta: { productId, milestoneId } + }) + throw error + }) + } +} diff --git a/src/projects/detail/components/timeline/Milestone/Milestone.jsx b/src/projects/detail/components/timeline/Milestone/Milestone.jsx index e92e9cd0c..4e91be096 100644 --- a/src/projects/detail/components/timeline/Milestone/Milestone.jsx +++ b/src/projects/detail/components/timeline/Milestone/Milestone.jsx @@ -21,6 +21,8 @@ import MilestoneTypeFinalDesigns from '../milestones/MilestoneTypeFinalDesigns' import MilestoneTypeDelivery from '../milestones/MilestoneTypeDelivery' import MilestoneTypeFinalFixes from '../milestones/MilestoneTypeFinalFixes' import MilestoneTypeAddLinks from '../milestones/MilestoneTypeAddLinks' +import MilestoneTypeReporting from '../milestones/MilestoneTypeReporting' +import MilestoneTypeDeliverableReview from '../milestones/MilestoneTypeDeliverableReview' import DotIndicator from '../DotIndicator' import MobilePage from '../../../../../components/MobilePage/MobilePage' import MediaQuery from 'react-responsive' @@ -48,6 +50,7 @@ class Milestone extends React.Component { this.completeFinalFixesMilestone = this.completeFinalFixesMilestone.bind(this) this.extendMilestone = this.extendMilestone.bind(this) this.submitFinalFixesRequest = this.submitFinalFixesRequest.bind(this) + this.submitDeliverableFinalFixesRequest = this.submitDeliverableFinalFixesRequest.bind(this) this.milestoneEditorChanged = this.milestoneEditorChanged.bind(this) this.state = { @@ -157,7 +160,7 @@ class Milestone extends React.Component { } } - updateMilestoneContent(contentProps, metaDataProps) { + updateMilestoneContent(contentProps, metaDataProps, status) { const { updateMilestone, milestone } = this.props const updatedMilestone = { @@ -174,6 +177,10 @@ class Milestone extends React.Component { } } + if (status) { + updatedMilestone.status = status + } + updateMilestone(milestone.id, updatedMilestone) } @@ -216,6 +223,12 @@ class Milestone extends React.Component { submitFinalFixesRequest(milestone.id, finalFixRequests) } + submitDeliverableFinalFixesRequest(finalFixesRequest) { + const { submitDeliverableFinalFixesRequest, milestone } = this.props + + submitDeliverableFinalFixesRequest(milestone.id, finalFixesRequest) + } + render() { const { milestone, @@ -330,6 +343,7 @@ class Milestone extends React.Component { disableSubmitButton={this.state.disableSubmit} /> ) + return (
{(
)} @@ -491,6 +505,24 @@ class Milestone extends React.Component { /> ) } + + {!isEditing && !isUpdating && milestone.type === 'reporting' && ( + + )} + + {!isEditing && !isUpdating && (milestone.type === 'deliverable-review' || milestone.type === 'final-deliverable-review' || milestone.type === 'deliverable-final-fixes') && ( + + )}
) @@ -504,6 +536,7 @@ Milestone.propTypes = { milestone: PT.object.isRequired, submitFinalFixesRequest: PT.func.isRequired, updateMilestone: PT.func.isRequired, + submitDeliverableFinalFixesRequest: PT.func.isRequired, } export default Milestone diff --git a/src/projects/detail/components/timeline/Timeline/Timeline.jsx b/src/projects/detail/components/timeline/Timeline/Timeline.jsx index 12ed9cbb8..96021dbb9 100644 --- a/src/projects/detail/components/timeline/Timeline/Timeline.jsx +++ b/src/projects/detail/components/timeline/Timeline/Timeline.jsx @@ -25,6 +25,7 @@ class Timeline extends React.Component { this.completeFinalFixesMilestone = this.completeFinalFixesMilestone.bind(this) this.extendMilestone = this.extendMilestone.bind(this) this.submitFinalFixesRequest = this.submitFinalFixesRequest.bind(this) + this.submitDeliverableFinalFixesRequest = this.submitDeliverableFinalFixesRequest.bind(this) } componentWillReceiveProps() { @@ -91,6 +92,16 @@ class Timeline extends React.Component { submitFinalFixesRequest(product.id, timeline.id, milestoneId, finalFixRequests) } + submitDeliverableFinalFixesRequest(milestoneId, finalFixesRequest) { + const { + product, + submitDeliverableFinalFixesRequest, + timeline, + } = this.props + + submitDeliverableFinalFixesRequest(product.id, timeline.id, milestoneId, finalFixesRequest) + } + render() { const { currentUser, @@ -123,6 +134,7 @@ class Timeline extends React.Component { extendMilestone={this.extendMilestone} submitFinalFixesRequest={this.submitFinalFixesRequest} completeFinalFixesMilestone={this.completeFinalFixesMilestone} + submitDeliverableFinalFixesRequest={this.submitDeliverableFinalFixesRequest} //$TODO convert the below logic more optimized way previousMilestone={_.find(orderedMilestones, m => m.order === milestone.order-1) && _.find(orderedMilestones, m => m.order === milestone.order-1).type} @@ -146,6 +158,7 @@ Timeline.propType = { updateProductMilestone: PT.func.isRequired, completeProductMilestone: PT.func.isRequired, extendProductMilestone: PT.func.isRequired, + submitDeliverableFinalFixesRequest: PT.func.isRequired, } export default Timeline diff --git a/src/projects/detail/components/timeline/milestones/MilestoneTypeDeliverableReview/MilestoneTypeDeliverableReview.jsx b/src/projects/detail/components/timeline/milestones/MilestoneTypeDeliverableReview/MilestoneTypeDeliverableReview.jsx new file mode 100644 index 000000000..79e322207 --- /dev/null +++ b/src/projects/detail/components/timeline/milestones/MilestoneTypeDeliverableReview/MilestoneTypeDeliverableReview.jsx @@ -0,0 +1,368 @@ +/** + * Milestone type `deliverable-review`, `final-deliverable-review`, `deliverable-final-fixes` + */ +import React from 'react' +import PT from 'prop-types' +import _ from 'lodash' +import cn from 'classnames' + +import DotIndicator from '../../DotIndicator' +import LinkList from '../../LinkList' +import LinkItem from '../../LinkItem' +import LinkItemForm from '../../LinkItemForm' +import MilestoneDescription from '../../MilestoneDescription' +import { withMilestoneExtensionRequest } from '../../MilestoneExtensionRequest' +import { getMilestoneStatusText } from '../../../../../../helpers/milestoneHelper' + +import { + MILESTONE_STATUS +} from '../../../../../../config/constants' + +import './MilestoneTypeDeliverableReview.scss' +import { hasPermission } from '../../../../../../helpers/permissions' +import { PERMISSIONS } from '../../../../../../config/permissions' + +class MilestoneTypeDeliverableReview extends React.Component { + constructor(props) { + super(props) + + this.state = { + isAddingDeliverableUpdate: false, + isAddingFeedbackLink: false, + deliverableUpdateText: '', + feedbackLink: '', + isAdddingFinalFixesRequest: false, + finalFixesRequest: '', + } + + this.onClickAddDeliverableReview = this.onClickAddDeliverableReview.bind(this) + this.onClickAddFeedbackLink = this.onClickAddFeedbackLink.bind(this) + this.onDeliverableUpdateTextChange = this.onDeliverableUpdateTextChange.bind(this) + this.onClickSaveDeliverableUpdate = this.onClickSaveDeliverableUpdate.bind(this) + this.onClickCancelAddDeliverableUpdate = this.onClickCancelAddDeliverableUpdate.bind(this) + this.updatedSubmissionUrl = this.updatedSubmissionUrl.bind(this) + this.removeSubmissionUrl = this.removeSubmissionUrl.bind(this) + this.onSubmitFeedbackLink = this.onSubmitFeedbackLink.bind(this) + this.onAddFeedbackLinkCancel = this.onAddFeedbackLinkCancel.bind(this) + this.onDeleteFeedbackLink = this.onDeleteFeedbackLink.bind(this) + this.onClickReviewComplete = this.onClickReviewComplete.bind(this) + + this.onClickRequestFixes = this.onClickRequestFixes.bind(this) + this.onClickCancelFinalFixesRequest = this.onClickCancelFinalFixesRequest.bind(this) + this.onClickSubmitRequest = this.onClickSubmitRequest.bind(this) + this.onFinalFixesRequestTextChange = this.onFinalFixesRequestTextChange.bind(this) + } + + onClickAddDeliverableReview() { + const { milestone } = this.props + + this.setState({ + isAddingDeliverableUpdate: true, + deliverableUpdateText: _.get(milestone, 'details.content.deliverableUpdate'), + }) + } + + onClickAddFeedbackLink() { + const { milestone } = this.props + + this.setState({ + isAddingFeedbackLink: true, + feedbackLink: _.get(milestone, 'details.content.feedbackLink'), + }) + } + + onAddFeedbackLinkCancel() { + this.setState({ + isAddingFeedbackLink: false, + }) + } + + onClickCancelAddDeliverableUpdate() { + this.setState({ + isAddingDeliverableUpdate: false, + }) + } + + onDeliverableUpdateTextChange(event) { + this.setState({ + deliverableUpdateText: event.target.value, + }) + } + + onClickSaveDeliverableUpdate() { + const { deliverableUpdateText } = this.state + const { updateMilestoneContent } = this.props + + updateMilestoneContent({ deliverableUpdate: deliverableUpdateText }) + } + + updatedSubmissionUrl(values, linkIndex) { + const { milestone, updateMilestoneContent } = this.props + + const submissionLinks = [..._.get(milestone, 'details.content.submissionLinks', [])] + + values.type = 'marvelapp' + + if (typeof linkIndex === 'number') { + submissionLinks.splice(linkIndex, 1, values) + } else { + submissionLinks.push(values) + } + + updateMilestoneContent({ + submissionLinks + }) + } + + removeSubmissionUrl(linkIndex) { + if (!window.confirm('Are you sure you want to remove this link?')) { + return + } + + const { milestone, updateMilestoneContent } = this.props + const submissionLinks = [..._.get(milestone, 'details.content.submissionLinks', [])] + + submissionLinks.splice(linkIndex, 1) + + updateMilestoneContent({ + submissionLinks + }) + } + + onDeleteFeedbackLink() { + if (!window.confirm('Are you sure you want to remove this link?')) { + return + } + + const { updateMilestoneContent } = this.props + + updateMilestoneContent({ + feedbackLink: '', + }) + } + + onSubmitFeedbackLink({ url }) { + const { updateMilestoneContent } = this.props + + updateMilestoneContent({ + feedbackLink: url, + }) + } + + onClickReviewComplete() { + const { completeMilestone } = this.props + + completeMilestone() + } + + onClickRequestFixes() { + this.setState({ + isAdddingFinalFixesRequest: true, + }) + } + + onClickCancelFinalFixesRequest() { + this.setState({ + isAdddingFinalFixesRequest: false, + }) + } + + onClickSubmitRequest() { + const { submitDeliverableFinalFixesRequest } = this.props + const { finalFixesRequest } = this.state + + submitDeliverableFinalFixesRequest(finalFixesRequest) + } + + onFinalFixesRequestTextChange(event) { + this.setState({ + finalFixesRequest: event.target.value, + }) + } + + render() { + const { + milestone, + theme, + } = this.props + const { + isAddingDeliverableUpdate, + isAddingFeedbackLink, + deliverableUpdateText, + feedbackLink, + + isAdddingFinalFixesRequest, + finalFixesRequest, + } = this.state + + const isActive = milestone.status === MILESTONE_STATUS.ACTIVE + + const canManage = hasPermission(PERMISSIONS.MANAGE_MILESTONE) + const canAcceptFinalDelivery = hasPermission(PERMISSIONS.ACCEPT_MILESTONE_FINAL_DELIVERY) + + const milestoneDeliverableFinalFixesRequest = _.get(milestone, 'details.content.finalFixesRequest', '') + const milestoneDeliverableUpdate = _.get(milestone, 'details.content.deliverableUpdate', '') + const milestoneFeedbackLink = _.get(milestone, 'details.content.feedbackLink', '') + const milestoneSubmissionLinks = _.get(milestone, 'details.content.submissionLinks', []) + + return ( +
+ + + + +
+
+ + {/* For milestone type of 'deliverable-final-fixes' specifically */} + {milestone.type === 'deliverable-final-fixes' && ( +
+
{milestoneDeliverableFinalFixesRequest}
+
+ )} + + {/* Deliverable update */} + {isAddingDeliverableUpdate ? ( +
+