diff --git a/src/api/projects.js b/src/api/projects.js index 17cc966da..950a39e7c 100644 --- a/src/api/projects.js +++ b/src/api/projects.js @@ -223,3 +223,8 @@ export function deleteProjectPhase(projectId, phaseId) { return axios.delete(`${PROJECTS_API_URL}/v5/projects/${projectId}/phases/${phaseId}`) .then(() => ({ projectId, phaseId })) } + +export function deleteBulkProjectPhase(projectId, phaseIds) { + return axios.delete(`${PROJECTS_API_URL}/v5/projects/${projectId}/phases`, { data: { phaseIds } }) + .then(() => ({ phaseIds })) +} diff --git a/src/config/constants.js b/src/config/constants.js index 0b27005a0..420aeda9f 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -300,6 +300,11 @@ export const DELETE_PROJECT_PHASE_PENDING = 'DELETE_PROJECT_PHASE_PENDING' export const DELETE_PROJECT_PHASE_FAILURE = 'DELETE_PROJECT_PHASE_FAILURE' export const DELETE_PROJECT_PHASE_SUCCESS = 'DELETE_PROJECT_PHASE_SUCCESS' +export const DELETE_BULK_PROJECT_PHASE = 'DELETE_BULK_PROJECT_PHASE' +export const DELETE_BULK_PROJECT_PHASE_PENDING = 'DELETE_BULK_PROJECT_PHASE_PENDING' +export const DELETE_BULK_PROJECT_PHASE_FAILURE = 'DELETE_BULK_PROJECT_PHASE_FAILURE' +export const DELETE_BULK_PROJECT_PHASE_SUCCESS = 'DELETE_BULK_PROJECT_PHASE_SUCCESS' + export const UPDATE_PRODUCT = 'UPDATE_PRODUCT' export const UPDATE_PRODUCT_PENDING = 'UPDATE_PRODUCT_PENDING' export const UPDATE_PRODUCT_SUCCESS = 'UPDATE_PRODUCT_SUCCESS' diff --git a/src/config/permissions.js b/src/config/permissions.js index 82e33250d..0b26002e5 100644 --- a/src/config/permissions.js +++ b/src/config/permissions.js @@ -249,7 +249,7 @@ export const PERMISSIONS = { description: 'Remove copilots form the project.', }, projectRoles: [ - ..._.difference(PROJECT_ALL, [PROJECT_ROLE_COPILOT, PROJECT_ROLE_CUSTOMER]) + ..._.difference(PROJECT_ALL, [PROJECT_ROLE_CUSTOMER]) ], topcoderRoles: [ ...TOPCODER_ADMINS, diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js index 6bcb8aff7..aedb88427 100644 --- a/src/projects/actions/project.js +++ b/src/projects/actions/project.js @@ -7,6 +7,7 @@ import { getProjectById, updateProject as updateProjectAPI, deleteProject as deleteProjectAPI, deleteProjectPhase as deleteProjectPhaseAPI, + deleteBulkProjectPhase as deleteBulkProjectPhaseAPI, getProjectPhases, updateProduct as updateProductAPI, updatePhase as updatePhaseAPI, @@ -79,7 +80,8 @@ import { CUSTOMER_APPROVE_MILESTONE_APPROVE_SUCCESS, CUSTOMER_APPROVE_MILESTONE_REJECT_FAILURE, CUSTOMER_APPROVE_MILESTONE_APPROVE_FAILURE, - CUSTOMER_APPROVE_MILESTONE_REJECT_SUCCESS + CUSTOMER_APPROVE_MILESTONE_REJECT_SUCCESS, + DELETE_BULK_PROJECT_PHASE } from '../../config/constants' import { updateProductMilestone, @@ -334,6 +336,11 @@ export function createProjectPhaseAndProduct(project, productTemplate, status = description: productTemplate.description, productTemplateId: productTemplate.id, } + + if(productTemplate.members) { + param.members = productTemplate.members + } + if (startDate) { param['startDate'] = startDate.format('YYYY-MM-DD') } @@ -441,6 +448,15 @@ export function deleteProjectPhase(projectId, phaseId) { } } +export function deleteBulkProjectPhase(projectId, phaseIds) { + return (dispatch) => { + return dispatch({ + type: DELETE_BULK_PROJECT_PHASE, + payload: deleteBulkProjectPhaseAPI(projectId, phaseIds) + }) + } +} + export function updateProject(projectId, updatedProps, updateExisting = false) { return (dispatch) => { return dispatch({ diff --git a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx index 10a37ccfe..c96add512 100644 --- a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx +++ b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx @@ -39,6 +39,7 @@ class CreateSimplePlan extends React.Component { onChangeMilestones, onSaveMilestone, onRemoveMilestone, + onRemoveAllMilestones, onGetChallenges, onApproveMilestones, isProjectLive, @@ -78,6 +79,7 @@ class CreateSimplePlan extends React.Component { onChangeMilestones={onChangeMilestones} onSaveMilestone={onSaveMilestone} onRemoveMilestone={onRemoveMilestone} + onRemoveAllMilestones={onRemoveAllMilestones} onApproveMilestones={onApproveMilestones} projectMembers={project.members} project={project} @@ -97,6 +99,7 @@ CreateSimplePlan.propTypes = { onChangeMilestones: PT.func, onSaveMilestone: PT.func, onRemoveMilestone: PT.func, + onRemoveAllMilestones: PT.func, onGetChallenges: PT.func, onApproveMilestones: PT.func, isCustomer: PT.bool, diff --git a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx index 31d19298f..0fd2b913b 100644 --- a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx +++ b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx @@ -16,7 +16,7 @@ import MilestoneMoveDateButton from '../components/MilestoneMoveDateButton' import * as milestoneHelper from '../components/helpers/milestone' import IconUnselect from '../../../../../assets/icons/icon-disselect.svg' import IconCopilot from '../../../../../assets/icons/icon-copilot.svg' -import { CHALLENGE_ID_MAPPING, PHASE_STATUS_IN_REVIEW } from '../../../../../config/constants' +import { CHALLENGE_ID_MAPPING, PHASE_STATUS_IN_REVIEW, PROJECT_STATUS_CANCELLED, PROJECT_STATUS_COMPLETED } from '../../../../../config/constants' import './ManageMilestones.scss' import MilestoneApprovalButton from '../components/MilestoneApprovalButton' @@ -84,11 +84,14 @@ class ManageMilestones extends React.Component { } onDeleteAll() { - const { milestones, onRemoveMilestone } = this.props - const seletedMilestones = _.filter(milestones, m => m.selected) - _.forEach(seletedMilestones, m => { - onRemoveMilestone(m.id) - }) + const { milestones, onRemoveAllMilestones } = this.props + const selectedPhases = _.filter(milestones, m => m.selected) + + if (selectedPhases.length) { + const { projectId } = selectedPhases[0] + const phaseIds = selectedPhases.map(m => m.id) + onRemoveAllMilestones(projectId, phaseIds) + } } onUnselectAll() { @@ -256,8 +259,11 @@ class ManageMilestones extends React.Component { onChangeMilestones, isUpdatable, isCustomer, + project, } = this.props + const hideCheckbox = project.status === PROJECT_STATUS_CANCELLED || project.status === PROJECT_STATUS_COMPLETED + // const isNeedApproval = project.status === PROJECT_STATUS_IN_REVIEW const isNeedApproval = !milestones.filter(ms => ms.selected === true).find(ms => !(ms.status === PHASE_STATUS_IN_REVIEW)) const canShowApproval = isCustomer && isNeedApproval @@ -325,7 +331,7 @@ class ManageMilestones extends React.Component { - {/* CHECKBOX */} + {hideCheckbox ? null : }{/* CHECKBOX */} {/* MILESTONE */} {/* DESCRIPTION */} {/* START DATE */} @@ -339,6 +345,7 @@ class ManageMilestones extends React.Component { milestones={milestones} onChangeMilestones={onChangeMilestones} isUpdatable={isUpdatable || isCustomer} + hideCheckbox={hideCheckbox} /> @@ -365,6 +372,7 @@ class ManageMilestones extends React.Component { onApprove={this.onApprove} phaseMembers={milestone.members} isApproving={milestonesInApproval.indexOf(milestone.id) !== -1} + hideCheckbox={hideCheckbox} />, ...this.renderChallengeTable(milestone) ] @@ -383,6 +391,7 @@ ManageMilestones.propTypes = { onChangeMilestones: PT.func, onSaveMilestone: PT.func, onRemoveMilestone: PT.func, + onRemoveAllMilestones: PT.func, onGetChallenges: PT.func, onApproveMilestones: PT.func, projectMembers: PT.arrayOf(PT.shape()), diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx index 7e940a433..b59849839 100644 --- a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx +++ b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx @@ -9,7 +9,7 @@ import './MilestoneHeaderRow.scss' const TCFormFields = FormsyForm.Fields -function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable }) { +function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable, hideCheckbox }) { const checked = milestones.reduce( (selected, milestone) => selected = selected && milestone.selected, milestones.length > 0 @@ -31,16 +31,18 @@ function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable }) { return ( - {isUpdatable ? + + } @@ -54,6 +56,7 @@ function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable }) { MilestoneHeaderRow.propTypes = { onChangeMilestones: PT.func, + hideCheckbox: PT.bool, } export default MilestoneHeaderRow diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx index 47ebf9e32..f14a3c4c5 100644 --- a/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx +++ b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx @@ -45,7 +45,8 @@ function MilestoneRow({ phaseMembers, disableDeleteAction, isCustomer, - isApproving + isApproving, + hideCheckbox }) { const isNeedApproval = milestone.status === PHASE_STATUS_IN_REVIEW const showApproval = isCustomer && isNeedApproval @@ -58,11 +59,12 @@ function MilestoneRow({ let milestoneRef let startDateRef let endDateRef + const tdEl = hideCheckbox ? null : {isUpdatable ? :
: null} - - { - value ? selectAll() : unselectAll() - }} - /> - + { + hideCheckbox ? null : + { + value ? selectAll() : unselectAll() + }} + /> + MILESTONE DESCRIPTION START DATE return edit ? (
: null} - {isEditingMilestone ? : + {(isEditingMilestone || hideCheckbox) ? : {isUpdatable ? onExpand(!isExpand, milestone)}>{isExpand ? : }} - {isEditingMilestone ? : + {(isEditingMilestone || hideCheckbox) ? tdEl : phase.id === id) const phase = createGameplanPhases[index] + - /* - * @return {Promise} The updated phase members + /** + * Helper function to get changes in members */ - const updatePhaseMembers = (projectId, phaseId) => { + const getUpdatedPhaseMembers = () => { const phaseMembers = _.get(phase, 'members', []) const oldPhaseMembers = _.get(phase, 'origin.members', []) - if (phaseMembers.length !== oldPhaseMembers.length || - _.differenceBy(phaseMembers, oldPhaseMembers, member => member.userId).length !== 0) { - return this.props.updatePhaseMembers( - projectId, - phaseId, - phaseMembers.map(member => member.userId) - ).then(() => phaseMembers) // ignore the result from backend + const updatedPhaseMembers = + _.differenceBy(phaseMembers, oldPhaseMembers, member => member.userId) + if(phaseMembers.length !== oldPhaseMembers.length || updatedPhaseMembers.length !== 0) { + return phaseMembers } - - return Promise.resolve(phaseMembers) + return null } if (`${phase.id}`.startsWith('new-milestone')) { @@ -217,8 +215,11 @@ class DashboardContainer extends React.Component { productTemplate.description = phase.description.trim() } - const projectId = project.id - let phaseId + const phaseMembers = getUpdatedPhaseMembers() + if(phaseMembers !== undefined) { + productTemplate.members = phaseMembers.map(member => member.userId) + } + createPhaseWithoutTimeline( project, productTemplate, @@ -226,26 +227,13 @@ class DashboardContainer extends React.Component { moment.utc(phase.startDate), moment.utc(phase.endDate) ).then(({ action }) => { - phaseId = action.payload.phase.id // reload phase const updatedCreateGameplanPhases = [...this.state.createGameplanPhases] updatedCreateGameplanPhases.splice(index, 1, { ...action.payload.phase, selected: phase.selected, }) - this.setState({ createGameplanPhases: updatedCreateGameplanPhases }, () => { - updatePhaseMembers(projectId, phaseId) - .then((members) => { - // reload members - const updatedPhases = [...this.state.createGameplanPhases] - updatedPhases.splice(index, 1, { - ...updatedPhases[index], - members - }) - this.setState({ createGameplanPhases: updatedPhases }) - }) - }) - + this.setState({ createGameplanPhases: updatedCreateGameplanPhases }) }) } else { const updateParam = { @@ -259,14 +247,16 @@ class DashboardContainer extends React.Component { updateParam.description = phase.description.trim() } - Promise.all([ - updatePhase( - phase.projectId, - phase.id, - updateParam - ), - updatePhaseMembers(phase.projectId, phase.id) - ]).then(([{ action }, members]) => { + const phaseMembers = getUpdatedPhaseMembers() + if(phaseMembers !== undefined) { + updateParam.members = phaseMembers.map(member => member.userId) + } + + updatePhase( + phase.projectId, + phase.id, + updateParam + ).then(([{ action }]) => { const updatedCreateGameplanPhases = [...this.state.createGameplanPhases] const idx = updatedCreateGameplanPhases.findIndex(phase => phase.id === action.payload.id) @@ -275,7 +265,8 @@ class DashboardContainer extends React.Component { ...action.payload, edit: this.state.createGameplanPhases[idx].edit, selected: this.state.createGameplanPhases[idx].selected, - members + products: this.state.createGameplanPhases[idx].products, + challenges: this.state.createGameplanPhases[idx].challenges, }) this.setState({ createGameplanPhases: updatedCreateGameplanPhases }) }) @@ -321,6 +312,21 @@ class DashboardContainer extends React.Component { }) } + onRemoveAllMilestones(projectId, phaseIds) { + this.props.deleteBulkProjectPhase( + projectId, + phaseIds + ).then(() => { + if (!this.state.createGameplanPhases) { + return + } + + // remove phases + const newGameplanPhases = this.state.createGameplanPhases.filter(phase => !phaseIds.includes(phase.id)) + this.setState({createGameplanPhases: newGameplanPhases}) + }) + } + render() { const { @@ -523,6 +529,7 @@ class DashboardContainer extends React.Component { onSaveMilestone={this.onSaveMilestone} onGetChallenges={this.onGetChallenges} onRemoveMilestone={this.onRemoveMilestone} + onRemoveAllMilestones={this.onRemoveAllMilestones} onApproveMilestones={this.onApproveMilestones} /> ) @@ -575,6 +582,7 @@ const mapDispatchToProps = { updateProductAttachment, removeProductAttachment, deleteProjectPhase, + deleteBulkProjectPhase, expandProjectPhase, collapseProjectPhase, collapseAllProjectPhases, @@ -585,7 +593,6 @@ const mapDispatchToProps = { updateProjectAttachment, removeProjectAttachment, updatePhase, - updatePhaseMembers, executePhaseApproval, approveMilestone } diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index d6b11b9f0..b04a79c62 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -26,7 +26,7 @@ import { ACCEPT_OR_REFUSE_INVITE_SUCCESS, ACCEPT_OR_REFUSE_INVITE_FAILURE, ACCEPT_OR_REFUSE_INVITE_PENDING, UPLOAD_PROJECT_ATTACHMENT_FILES, DISCARD_PROJECT_ATTACHMENT, CHANGE_ATTACHMENT_PERMISSION, CREATE_SCOPE_CHANGE_REQUEST_SUCCESS, APPROVE_SCOPE_CHANGE_SUCCESS, REJECT_SCOPE_CHANGE_SUCCESS, CANCEL_SCOPE_CHANGE_SUCCESS, ACTIVATE_SCOPE_CHANGE_SUCCESS, - LOAD_PROJECT_MEMBERS_SUCCESS, LOAD_PROJECT_MEMBER_INVITES_SUCCESS, LOAD_PROJECT_MEMBER_SUCCESS, CREATE_PROJECT_PHASE_PENDING, CREATE_PROJECT_PHASE_SUCCESS, CUSTOMER_APPROVE_MILESTONE_PENDING, CUSTOMER_APPROVE_MILESTONE_FINISHED, + LOAD_PROJECT_MEMBERS_SUCCESS, LOAD_PROJECT_MEMBER_INVITES_SUCCESS, LOAD_PROJECT_MEMBER_SUCCESS, CREATE_PROJECT_PHASE_PENDING, CREATE_PROJECT_PHASE_SUCCESS, CUSTOMER_APPROVE_MILESTONE_PENDING, CUSTOMER_APPROVE_MILESTONE_FINISHED, DELETE_BULK_PROJECT_PHASE_PENDING, DELETE_BULK_PROJECT_PHASE_SUCCESS, } from '../../config/constants' import _ from 'lodash' import update from 'react-addons-update' @@ -519,6 +519,7 @@ export const projectState = function (state=initialState, action) { case UPDATE_PROJECT_PENDING: case UPDATE_PHASE_PENDING: case DELETE_PROJECT_PHASE_PENDING: + case DELETE_BULK_PROJECT_PHASE_PENDING: return Object.assign({}, state, { isLoading: false, processing: true, @@ -562,6 +563,18 @@ export const projectState = function (state=initialState, action) { }) } + case DELETE_BULK_PROJECT_PHASE_SUCCESS: { + const { phaseIds } = action.payload + + const newPhases = state.phases.filter(phase => !phaseIds.includes(phase.id)) + + return Object.assign({}, state, { + phases: newPhases, + phasesNonDirty: newPhases, + processing: false, + }) + } + case UPDATE_PRODUCT_SUCCESS: return { ...state, diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index da1b345e3..6de87290c 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -77,7 +77,8 @@ import { CUSTOMER_APPROVE_MILESTONE_APPROVE_SUCCESS, CUSTOMER_APPROVE_MILESTONE_REJECT_SUCCESS, CUSTOMER_APPROVE_MILESTONE_APPROVE_FAILURE, - CUSTOMER_APPROVE_MILESTONE_REJECT_FAILURE + CUSTOMER_APPROVE_MILESTONE_REJECT_FAILURE, + DELETE_BULK_PROJECT_PHASE_SUCCESS } from '../config/constants' /* eslint-enable no-unused-vars */ @@ -112,6 +113,16 @@ export default function(state = {}, action) { return state } + case DELETE_BULK_PROJECT_PHASE_SUCCESS: { + if (state.project.version === 'v4') { + Alert.success('Project milestones deleted.') + } else { + Alert.success('Project phases deleted.') + } + + return state + } + case DELETE_PROJECT_SUCCESS: Alert.success('Project deleted.') return state