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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ export function updateProject(projectId, updatedProps, updateExisting) {
})
}

/**
* Create scope change request for the given project with the given details
* @param {integer} projectId project Id
* @param {object} request scope change request object
* @return {promise} created scope change request
*/
export function createScopeChangeRequest(projectId, request) {
return axios.post(`${PROJECTS_API_URL}/v4/projects/${projectId}/scopeChangeRequests`, { param: request })
.then(resp => {
return _.get(resp.data, 'result.content')
})
}/**
* Create scope change request for the given project with the given details
* @param {integer} projectId project Id
* @param {integer} requestId scope change request Id
* @param {object} updatedProps updated request properties
* @return {promise} updated request
*/
export function updateScopeChangeRequest(projectId, requestId, updatedProps) {
return axios.patch(`${PROJECTS_API_URL}/v4/projects/${projectId}/scopeChangeRequests/${requestId}`, { param: updatedProps })
.then(resp => {
return _.get(resp.data, 'result.content')
})
}

/**
* Update phase using patch
* @param {integer} projectId project Id
Expand Down
34 changes: 34 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,31 @@ export const UPDATE_PROJECT_PENDING = 'UPDATE_PROJECT_PENDING'
export const UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS'
export const UPDATE_PROJECT_FAILURE = 'UPDATE_PROJECT_FAILURE'

export const CREATE_SCOPE_CHANGE_REQUEST = 'CREATE_SCOPE_CHANGE_REQUEST'
export const CREATE_SCOPE_CHANGE_REQUEST_PENDING = 'CREATE_SCOPE_CHANGE_REQUEST_PENDING'
export const CREATE_SCOPE_CHANGE_REQUEST_SUCCESS = 'CREATE_SCOPE_CHANGE_REQUEST_SUCCESS'
export const CREATE_SCOPE_CHANGE_REQUEST_FAILURE = 'CREATE_SCOPE_CHANGE_REQUEST_FAILURE'

export const APPROVE_SCOPE_CHANGE = 'APPROVE_SCOPE_CHANGE'
export const APPROVE_SCOPE_CHANGE_PENDING = 'APPROVE_SCOPE_CHANGE_PENDING'
export const APPROVE_SCOPE_CHANGE_SUCCESS = 'APPROVE_SCOPE_CHANGE_SUCCESS'
export const APPROVE_SCOPE_CHANGE_FAILURE = 'APPROVE_SCOPE_CHANGE_FAILURE'

export const REJECT_SCOPE_CHANGE = 'REJECT_SCOPE_CHANGE'
export const REJECT_SCOPE_CHANGE_PENDING = 'REJECT_SCOPE_CHANGE_PENDING'
export const REJECT_SCOPE_CHANGE_SUCCESS = 'REJECT_SCOPE_CHANGE_SUCCESS'
export const REJECT_SCOPE_CHANGE_FAILURE = 'REJECT_SCOPE_CHANGE_FAILURE'

export const CANCEL_SCOPE_CHANGE = 'CANCEL_SCOPE_CHANGE'
export const CANCEL_SCOPE_CHANGE_PENDING = 'CANCEL_SCOPE_CHANGE_PENDING'
export const CANCEL_SCOPE_CHANGE_SUCCESS = 'CANCEL_SCOPE_CHANGE_SUCCESS'
export const CANCEL_SCOPE_CHANGE_FAILURE = 'CANCEL_SCOPE_CHANGE_FAILURE'

export const ACTIVATE_SCOPE_CHANGE = 'ACTIVATE_SCOPE_CHANGE'
export const ACTIVATE_SCOPE_CHANGE_PENDING = 'ACTIVATE_SCOPE_CHANGE_PENDING'
export const ACTIVATE_SCOPE_CHANGE_SUCCESS = 'ACTIVATE_SCOPE_CHANGE_SUCCESS'
export const ACTIVATE_SCOPE_CHANGE_FAILURE = 'ACTIVATE_SCOPE_CHANGE_FAILURE'

export const UPDATE_PHASE = 'UPDATE_PHASE'
export const UPDATE_PHASE_PENDING = 'UPDATE_PHASE_PENDING'
export const UPDATE_PHASE_FAILURE = 'UPDATE_PHASE_FAILURE'
Expand Down Expand Up @@ -522,6 +547,15 @@ export const PROJECT_STATUS_COMPLETED = 'completed'
export const PROJECT_STATUS_CANCELLED = 'cancelled'
export const PROJECT_STATUS_PAUSED = 'paused'

/*
* Scope change request status
*/
export const SCOPE_CHANGE_REQ_STATUS_PENDING = 'pending'
export const SCOPE_CHANGE_REQ_STATUS_APPROVED = 'approved'
export const SCOPE_CHANGE_REQ_STATUS_REJECTED = 'rejected'
export const SCOPE_CHANGE_REQ_STATUS_ACTIVATED = 'activated'
export const SCOPE_CHANGE_REQ_STATUS_CANCELED = 'canceled'


export const PHASE_STATUS_DRAFT = 'draft'
export const PHASE_STATUS_IN_REVIEW = 'in_review'
Expand Down
77 changes: 77 additions & 0 deletions src/projects/actions/project.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash'
import moment from 'moment'
import { flatten, unflatten } from 'flat'
import { getProjectById,
createProject as createProjectAPI,
createProjectWithStatus as createProjectWithStatusAPI,
Expand All @@ -11,6 +12,8 @@ import { getProjectById,
updateProduct as updateProductAPI,
updatePhase as updatePhaseAPI,
createProjectPhase,
createScopeChangeRequest as createScopeChangeRequestAPI,
updateScopeChangeRequest as updateScopeChangeRequestAPI,
} from '../../api/projects'
import {
getProjectInviteById,
Expand Down Expand Up @@ -48,6 +51,15 @@ import {
EXPAND_PROJECT_PHASE,
COLLAPSE_PROJECT_PHASE,
COLLAPSE_ALL_PROJECT_PHASES,
CREATE_SCOPE_CHANGE_REQUEST,
APPROVE_SCOPE_CHANGE,
REJECT_SCOPE_CHANGE,
CANCEL_SCOPE_CHANGE,
ACTIVATE_SCOPE_CHANGE,
SCOPE_CHANGE_REQ_STATUS_ACTIVATED,
SCOPE_CHANGE_REQ_STATUS_APPROVED,
SCOPE_CHANGE_REQ_STATUS_REJECTED,
SCOPE_CHANGE_REQ_STATUS_CANCELED,
} from '../../config/constants'
import {
updateProductMilestone,
Expand Down Expand Up @@ -319,6 +331,71 @@ export function updateProject(projectId, updatedProps, updateExisting = false) {
}
}

export function createScopeChangeRequest(projectId, request) {
const flatNewScope = flatten(request.newScope, { safe: true })
const emptyKeys = _.keys(flatNewScope).filter(key => {
const newValue = _.get(request.newScope, key)
const oldValue = _.get(request.oldScope, key)

const isUnChangedEmptyObject = _.isObject(newValue) && _.isEmpty(newValue) && (_.isObject(oldValue) && _.isEmpty(oldValue) || _.isNil(oldValue))
const isUnChangedEmptyString = newValue === '' && (oldValue === '' || _.isNil(oldValue))

return isUnChangedEmptyObject || isUnChangedEmptyString
})

const cleanedRequest = {
...request,
newScope: unflatten(_.omit(flatNewScope, emptyKeys))
}

return (dispatch) => {
return dispatch({
type: CREATE_SCOPE_CHANGE_REQUEST,
payload: createScopeChangeRequestAPI(projectId, cleanedRequest)
})
}
}

export function approveScopeChange(projectId, scopeChangeRequestId) {
const request = { status : SCOPE_CHANGE_REQ_STATUS_APPROVED }
return (dispatch) => {
return dispatch({
type: APPROVE_SCOPE_CHANGE,
payload: updateScopeChangeRequestAPI(projectId, scopeChangeRequestId, request)
})
}
}

export function rejectScopeChange(projectId, scopeChangeRequestId) {
const request = { status : SCOPE_CHANGE_REQ_STATUS_REJECTED }
return (dispatch) => {
return dispatch({
type: REJECT_SCOPE_CHANGE,
payload: updateScopeChangeRequestAPI(projectId, scopeChangeRequestId, request)
})
}
}

export function cancelScopeChange(projectId, scopeChangeRequestId) {
const request = { status: SCOPE_CHANGE_REQ_STATUS_CANCELED }
return (dispatch) => {
return dispatch({
type: CANCEL_SCOPE_CHANGE,
payload: updateScopeChangeRequestAPI(projectId, scopeChangeRequestId, request)
})
}
}

export function activateScopeChange(projectId, scopeChangeRequestId) {
const request = { status: SCOPE_CHANGE_REQ_STATUS_ACTIVATED }
return (dispatch) => {
return dispatch({
type: ACTIVATE_SCOPE_CHANGE,
payload: updateScopeChangeRequestAPI(projectId, scopeChangeRequestId, request)
})
}
}


/**
* Update phase info
Expand Down
27 changes: 21 additions & 6 deletions src/projects/detail/components/EditProjectForm/EditProjectForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { clean } from '../../../../helpers/utils'

import './EditProjectForm.scss'
import { PROJECT_STATUS_DRAFT, PROJECT_STATUS_IN_REVIEW, PROJECT_STATUS_COMPLETED } from '../../../../config/constants'

const FeaturePickerModal = ({ project, isEdittable, showFeaturesDialog, hideFeaturesDialog, saveFeatures, setValue }) => {
const setFormValue = (features, featureSeeAttached=false) => {
Expand Down Expand Up @@ -72,6 +73,7 @@ class EditProjectForm extends Component {
this.onLeave = this.onLeave.bind(this)
this.handleChange = this.handleChange.bind(this)
this.makeDeliveredPhaseReadOnly = this.makeDeliveredPhaseReadOnly.bind(this)
this.isScopeFreezed = this.isScopeFreezed.bind(this)

// init wizard to support dependant questions
const {
Expand Down Expand Up @@ -260,10 +262,19 @@ class EditProjectForm extends Component {
// this.props.submitHandler({ details })
}

isScopeFreezed() {
return [PROJECT_STATUS_DRAFT, PROJECT_STATUS_IN_REVIEW].indexOf(this.props.project.status) === -1
}

submit(model) {
this.setState({isSaving: true })
const modelWithoutHiddenValues = removeValuesOfHiddenNodes(this.state.template, model)
this.props.submitHandler(modelWithoutHiddenValues)
const scopeFreezed = this.isScopeFreezed()
this.props.submitHandler(modelWithoutHiddenValues, scopeFreezed)

if (scopeFreezed) {
this.refs.form.reset()
}
}

/**
Expand All @@ -277,7 +288,7 @@ class EditProjectForm extends Component {
}

makeDeliveredPhaseReadOnly(projectStatus) {
return projectStatus === 'completed'
return projectStatus === PROJECT_STATUS_COMPLETED
}

render() {
Expand All @@ -286,6 +297,7 @@ class EditProjectForm extends Component {
showHidden,
productTemplates,
productCategories,
pendingScopeChange,
isInsideDrawer,
disableAutoScrolling,
currentWizardStep,
Expand Down Expand Up @@ -329,10 +341,12 @@ class EditProjectForm extends Component {
currentWizardStep={currentWizardStep}
/>
<div className="section-footer section-footer-spec">
<button className="tc-btn tc-btn-primary tc-btn-md"
type="submit"
disabled={(!this.isChanged() || this.state.isSaving) || anySectionInvalid || !this.state.canSubmit || this.makeDeliveredPhaseReadOnly(project.status)}
>Save Changes</button>
{ !pendingScopeChange &&
<button className="tc-btn tc-btn-primary tc-btn-md"
type="submit"
disabled={(!this.isChanged() || this.state.isSaving) || anySectionInvalid || !this.state.canSubmit || this.makeDeliveredPhaseReadOnly(project.status)}
>{ this.isScopeFreezed() ? 'Submit Change Request' : 'Save Changes'}</button>
}
</div>
</div>
)
Expand Down Expand Up @@ -416,6 +430,7 @@ EditProjectForm.propTypes = {
shouldUpdateTemplate: PropTypes.bool,
isInsideDrawer: PropTypes.bool,
disableAutoScrolling: PropTypes.bool,
pendingScopeChange: PropTypes.object,
/**
* If `currentWizardStep` is defined, then edit form shows form in the wizard mode
* with this step as current, instead of showing all the sections.
Expand Down
Loading