diff --git a/src/components/User/UserTooltip.scss b/src/components/User/UserTooltip.scss
index 770d84f6f..8c909de06 100644
--- a/src/components/User/UserTooltip.scss
+++ b/src/components/User/UserTooltip.scss
@@ -49,6 +49,15 @@
}
}
+ &.size-50 {
+ .tooltip-target {
+ .Avatar {
+ height: 50px;
+ width: 50px;
+ }
+ }
+ }
+
.stack-avatar-0 {
z-index: 3;
}
diff --git a/src/config/constants.js b/src/config/constants.js
index 0c85a21a6..dcd197114 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -632,6 +632,16 @@ export const PHASE_STATUS = [
// {color: 'red', name: 'Paused', fullName: 'Phase is paused', value: PHASE_STATUS_PAUSED, order: 7, dropDownOrder: 7 }
]
+export const PHASE_STATUS_OPTIONS = [
+ { label: 'Draft', value: PHASE_STATUS_DRAFT },
+ { label: 'In Review', value: PHASE_STATUS_IN_REVIEW },
+ { label: 'Reviewed', value: PHASE_STATUS_REVIEWED },
+ { label: 'Active', value: PHASE_STATUS_ACTIVE },
+ { label: 'Completed', value: PHASE_STATUS_COMPLETED },
+ { label: 'Cancelled', value: PHASE_STATUS_CANCELLED },
+ { label: 'Phased', value: PHASE_STATUS_PAUSED },
+]
+
// this defines default criteria to filter projects for projects list
export const PROJECT_LIST_DEFAULT_CRITERIA = {
sort: 'lastActivityAt desc'
@@ -1228,4 +1238,4 @@ export const DEFAULT_NDA_UUID = process.env.DEFAULT_NDA_UUID
/**
* The minimal duration of a TaaS Job in weeks
*/
-export const TAAS_MIN_JOB_DURATION = 4
\ No newline at end of file
+export const TAAS_MIN_JOB_DURATION = 4
diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js
index 848e2540d..51278d20c 100644
--- a/src/projects/actions/project.js
+++ b/src/projects/actions/project.js
@@ -286,12 +286,14 @@ function createProductsTimelineAndMilestone(project) {
*
* @return {Promise} project
*/
-export function createProjectPhaseAndProduct(project, productTemplate, status = PHASE_STATUS_DRAFT, startDate, endDate, createTimeline = true) {
+export function createProjectPhaseAndProduct(project, productTemplate, status = PHASE_STATUS_DRAFT, startDate, endDate, createTimeline = true, budget, details) {
const param = {
status,
name: productTemplate.name,
description: productTemplate.description,
- productTemplateId: productTemplate.id
+ productTemplateId: productTemplate.id,
+ budget,
+ details,
}
if (startDate) {
param['startDate'] = startDate.format('YYYY-MM-DD')
@@ -356,12 +358,12 @@ function createPhaseAndMilestonesRequest(project, productTemplate, status = PHAS
* @param {*} startDate
* @param {*} endDate
*/
-export function createPhaseWithoutTimeline(project, productTemplate, status, startDate, endDate) {
+export function createPhaseWithoutTimeline(project, productTemplate, status, startDate, endDate, budget, details) {
return (dispatch) => {
console.log(CREATE_PROJECT_PHASE)
return dispatch({
type: CREATE_PROJECT_PHASE,
- payload: createProjectPhaseAndProduct(project, productTemplate, status, startDate, endDate, false)
+ payload: createProjectPhaseAndProduct(project, productTemplate, status, startDate, endDate, false, budget, details)
})
}
}
@@ -505,75 +507,78 @@ export function updatePhase(projectId, phaseId, updatedProps, phaseIndex) {
updatedProps.endDate = moment(updatedProps.endDate).format('YYYY-MM-DD')
}
+ let result
return dispatch({
type: UPDATE_PHASE,
payload: updatePhaseAPI(projectId, phaseId, updatedProps, phaseIndex).then()
- }).then(() => {
+ })
+ .then(res => result = res)
+ .then(() => {
// finds active milestone, if exists in timeline
- const activeMilestone = timeline ? _.find(timeline.milestones, m => m.status === PHASE_STATUS_ACTIVE) : null
- // this will be done after updating timeline (if timeline update is required)
- // or immediately if timeline update is not required
- // update product milestone strictly after updating timeline
- // otherwise it could happened like this:
- // - send request to update timeline
- // - send request to update milestone
- // - get updated milestone
- // - get updated timeline (without updated milestone)
- // so otherwise we can end up with the timeline without updated milestone
- const optionallyUpdateFirstMilestone = () => {
+ const activeMilestone = timeline ? _.find(timeline.milestones, m => m.status === PHASE_STATUS_ACTIVE) : null
+ // this will be done after updating timeline (if timeline update is required)
+ // or immediately if timeline update is not required
+ // update product milestone strictly after updating timeline
+ // otherwise it could happened like this:
+ // - send request to update timeline
+ // - send request to update milestone
+ // - get updated milestone
+ // - get updated timeline (without updated milestone)
+ // so otherwise we can end up with the timeline without updated milestone
+ const optionallyUpdateFirstMilestone = () => {
// update first product milestone only if
// - there is a milestone, obviously
// - phase's status is changed
// - phase's status is changed to active
// - there is not active milestone alreay (this can happen when phase is made active more than once
// e.g. Active => Paused => Active)
- if (projectVersion !== 'v4' && timeline && !activeMilestone && phaseActivated ) {
+ if (projectVersion !== 'v4' && timeline && !activeMilestone && phaseActivated ) {
+ dispatch(
+ updateProductMilestone(
+ productId,
+ timeline.id,
+ timeline.milestones[0].id,
+ {status:MILESTONE_STATUS.ACTIVE}
+ )
+ )
+ }
+ }
+
+ if (timeline && (startDateChanged || phaseActivated)) {
dispatch(
- updateProductMilestone(
+ updateProductTimeline(
productId,
timeline.id,
- timeline.milestones[0].id,
- {status:MILESTONE_STATUS.ACTIVE}
+ {
+ name: timeline.name,
+ startDate: updatedProps.startDate,
+ reference: timeline.reference,
+ referenceId: timeline.referenceId,
+ }
)
- )
+ ).then(optionallyUpdateFirstMilestone)
+ } else {
+ optionallyUpdateFirstMilestone()
}
- }
-
- if (timeline && (startDateChanged || phaseActivated)) {
- dispatch(
- updateProductTimeline(
- productId,
- timeline.id,
- {
- name: timeline.name,
- startDate: updatedProps.startDate,
- reference: timeline.reference,
- referenceId: timeline.referenceId,
- }
- )
- ).then(optionallyUpdateFirstMilestone)
- } else {
- optionallyUpdateFirstMilestone()
- }
- // update project caused by phase updates
- }).then(() => {
- const project = state.projectState.project
+ // update project caused by phase updates
+ }).then(() => {
+ const project = state.projectState.project
- // if one phase moved to ACTIVE status, make project ACTIVE too
- if (
- _.includes([PROJECT_STATUS_DRAFT, PROJECT_STATUS_IN_REVIEW, PROJECT_STATUS_REVIEWED], project.status) &&
+ // if one phase moved to ACTIVE status, make project ACTIVE too
+ if (
+ _.includes([PROJECT_STATUS_DRAFT, PROJECT_STATUS_IN_REVIEW, PROJECT_STATUS_REVIEWED], project.status) &&
phase.status !== PHASE_STATUS_ACTIVE &&
updatedProps.status === PHASE_STATUS_ACTIVE &&
hasPermission(PERMISSIONS.EDIT_PROJECT_STATUS)
- ) {
- dispatch(
- updateProject(projectId, {
- status: PROJECT_STATUS_ACTIVE
- }, true)
- )
- }
- })
+ ) {
+ dispatch(
+ updateProject(projectId, {
+ status: PROJECT_STATUS_ACTIVE
+ }, true)
+ )
+ }
+ }).then(() => result)
}
}
diff --git a/src/projects/detail/components/CreatePhaseForm/CreatePhaseForm.jsx b/src/projects/detail/components/CreatePhaseForm/CreatePhaseForm.jsx
index aec531f1a..d55363da5 100644
--- a/src/projects/detail/components/CreatePhaseForm/CreatePhaseForm.jsx
+++ b/src/projects/detail/components/CreatePhaseForm/CreatePhaseForm.jsx
@@ -225,7 +225,7 @@ class CreatePhaseForm extends React.Component {
const {
milestones
} = this.state
-
+
const ms = _.map(milestones, (m, index) => {
return (
diff --git a/src/projects/detail/components/ProjectPlanEmpty/ProjectPlanEmpty.jsx b/src/projects/detail/components/ProjectPlanEmpty/ProjectPlanEmpty.jsx
index 41e8c3755..4fed37e2b 100644
--- a/src/projects/detail/components/ProjectPlanEmpty/ProjectPlanEmpty.jsx
+++ b/src/projects/detail/components/ProjectPlanEmpty/ProjectPlanEmpty.jsx
@@ -7,11 +7,15 @@ import { hasPermission } from '../../../../helpers/permissions'
import './ProjectPlanEmpty.scss'
-const ProjectPlanEmpty = () => {
+const ProjectPlanEmpty = ({ version }) => {
return hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) ? (
Build Your Project Plan
-
Build your project plan in Connect to reflect delivery progress to the customer. Begin by clicking the "Add Phase" button, select the template that best matches your need, and modify the phase title and milestone dates prior to publishing to the customer.
+ {version === 'v4' ? (
+
Build your project plan in Connect to reflect delivery progress to the customer. Begin by clicking the "Add Milestone" button, and modify the milestone attributes prior to publishing to the customer.
+ ) : (
+
Build your project plan in Connect to reflect delivery progress to the customer. Begin by clicking the "Add Phase" button, select the template that best matches your need, and modify the phase title and milestone dates prior to publishing to the customer.
+ )}
Important Note: To move the project into 'Active' status, you must set at least one phase in Connect's Project Plan to be in 'Planned' status, which signifies to customers that delivery planning and execution has begun.
If you feel like you have more things to send over, or want to reach out to us, please drop us a line at support@topcoder.com. Thanks!
diff --git a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx
new file mode 100644
index 000000000..b059b4503
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.jsx
@@ -0,0 +1,110 @@
+/**
+ * Create simple project plan
+ */
+import React from 'react'
+import PT from 'prop-types'
+import _ from 'lodash'
+import GenericMenu from '../../../../../components/GenericMenu'
+// import ProjectDetailsWidget from '../ProjectDetailsWidget'
+import ManageMilestones from '../ManageMilestones'
+import * as milestoneHelper from '../components/helpers/milestone'
+
+import styles from './CreateSimplePlan.scss'
+
+const createTabs = ({ onClick } ) => ([
+ {
+ label: 'MILESTONES',
+ isActive: true,
+ onClick,
+ }
+])
+
+class CreateSimplePlan extends React.Component {
+ componentDidMount() {
+ const { project, milestones, loadMembers } = this.props
+
+ let copilotIds = []
+ milestones.forEach((milestone) => {
+ copilotIds = copilotIds.concat(_.get(milestone, 'details.copilots', []))
+ })
+
+ const projectMemberIds = project.members.map(member => member.userId)
+ const missingMemberIds = _.difference(copilotIds, projectMemberIds)
+ if (missingMemberIds.length) {
+ loadMembers(missingMemberIds)
+ }
+
+ const contentInnerElement = document.querySelector('.twoColsLayout-contentInner')
+ contentInnerElement.classList.add(styles['twoColsLayout-contentInner'])
+ }
+
+ componentWillUnmount() {
+ const contentInnerElement = document.querySelector('.twoColsLayout-contentInner')
+ contentInnerElement.classList.remove(styles['twoColsLayout-contentInner'])
+ }
+
+ render () {
+ const {
+ project,
+ // phases,
+ milestones,
+ onChangeMilestones,
+ onSaveMilestone,
+ onRemoveMilestone,
+ isProjectLive,
+ members,
+ isCustomer,
+ } = this.props
+ const onClickMilestonesTab = () => {}
+
+ if (milestones.length === 0) {
+ return isCustomer ? null : (
+
+ {
+ const newMilestone = milestoneHelper.createEmptyMilestone(new Date())
+ newMilestone.edit = true
+ onChangeMilestones([newMilestone])
+ }}
+ >
+ Add New Milestone
+
+
+ )
+ }
+
+ return (
+
+ )
+ }
+}
+
+CreateSimplePlan.propTypes = {
+ project: PT.shape(),
+ phases: PT.arrayOf(PT.shape()),
+ milestones: PT.arrayOf(PT.shape()),
+ onChangeMilestones: PT.func,
+ onSaveMilestone: PT.func,
+ onRemoveMilestone: PT.func,
+ members: PT.object,
+ isCustomer: PT.bool,
+}
+
+export default CreateSimplePlan
diff --git a/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.scss b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.scss
new file mode 100644
index 000000000..62b101e23
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/CreateSimplePlan/CreateSimplePlan.scss
@@ -0,0 +1,41 @@
+.milestones-container {
+ margin-top: 50px;
+}
+
+.tabs-header {
+ height: 0;
+ float: left;
+
+ nav {
+ justify-content: flex-start;
+ background-color: transparent;
+ }
+
+ ul {
+ > li:first-child {
+ margin-left: 0;
+ }
+
+ > li:last-child {
+ margin-right: 0;
+ }
+ }
+}
+
+.welcome-message {
+ margin: 0 auto;
+ max-width: 760px;
+}
+
+.add-new-milestone {
+ margin-top: 10px;
+ text-align: center;
+}
+
+.twoColsLayout-contentInner {
+ max-width: 947px;
+
+ @media screen and (min-width: 1280px) {
+ margin-left: calc((100% - 947px) * 0.41);
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/CreateSimplePlan/index.js b/src/projects/detail/components/SimplePlan/CreateSimplePlan/index.js
new file mode 100644
index 000000000..9505965d8
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/CreateSimplePlan/index.js
@@ -0,0 +1,2 @@
+import CreateSimplePlan from './CreateSimplePlan'
+export default CreateSimplePlan
diff --git a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx
new file mode 100644
index 000000000..241c378d4
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.jsx
@@ -0,0 +1,154 @@
+/**
+ * Manage milestones
+ */
+import React from 'react'
+import PT from 'prop-types'
+import FormsyForm from 'appirio-tech-react-components/components/Formsy'
+import MilestoneRow from '../components/MilestoneRow'
+import MilestoneHeaderRow from '../components/MilestoneHeaderRow'
+import * as milestoneHelper from '../components/helpers/milestone'
+// import IconGridView from '../../../../../assets/icons/ui-16px-2_grid-45-gray.svg'
+// import IconGnattView from '../../../../../assets/icons/icon-gnatt-gray.svg'
+
+import './ManageMilestones.scss'
+
+const Formsy = FormsyForm.Formsy
+
+class ManageMilestones extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.onSave = this.onSave.bind(this)
+ this.onChange = this.onChange.bind(this)
+ this.onAdd = this.onAdd.bind(this)
+ this.onRemove = this.onRemove.bind(this)
+ this.onDiscard = this.onDiscard.bind(this)
+ }
+
+ onChange(updatedMilestone) {
+ const { milestones, onChangeMilestones } = this.props
+ const index = milestones.findIndex(m => m.id === updatedMilestone.id)
+
+ const updatedMilestones = [...milestones]
+ updatedMilestones.forEach(milestone => milestone.editting = false)
+ updatedMilestones.splice(index, 1, updatedMilestone)
+ onChangeMilestones(updatedMilestones)
+ }
+
+ onDiscard(id) {
+ const { milestones, onChangeMilestones } = this.props
+ const index = milestones.findIndex(m => m.id === id)
+
+ const updatedMilestones = [...milestones]
+ updatedMilestones.splice(index, 1)
+ onChangeMilestones(updatedMilestones)
+ }
+
+ onAdd() {
+ const { milestones, onChangeMilestones } = this.props
+ const newMilestone = milestoneHelper.createEmptyMilestone(_.last(milestones).endDate)
+ newMilestone.edit = true
+
+ const updatedMilestones = [newMilestone, ...milestones]
+ onChangeMilestones(updatedMilestones)
+ }
+
+ onSave(id) {
+ const {onSaveMilestone} = this.props
+ onSaveMilestone(id)
+ }
+
+ onRemove(id) {
+ const {onRemoveMilestone} = this.props
+ onRemoveMilestone(id)
+ }
+
+ render() {
+ const {
+ milestones,
+ projectMembers,
+ onChangeMilestones,
+ isUpdatable,
+ members,
+ } = this.props
+
+ return (
+
+
+ {/*
+
+
+
+
+
+
*/}
+ {isUpdatable && (
+
+ ADD
+
+ )}
+ {/*
+ IMPORT MILESTONES
+
+
+ TEMPLATE
+ */}
+
+
+
+
+
+ {/* CHECKBOX */}
+ {/* MILESTONE */}
+ {/* DESCRIPTION */}
+ {/* START DATE */}
+ {/* END DATE */}
+ {/* STATUS */}
+ {/* BUDGET */}
+ {/* COPILOTS */}
+ {isUpdatable && ( )}{/* ACTION */}
+
+
+
+
+
+ {milestones.map((milestone) => (
+
+ ))}
+
+
+
+
+
+ )
+ }
+}
+
+ManageMilestones.propTypes = {
+ milestones: PT.arrayOf(PT.shape()),
+ onChangeMilestones: PT.func,
+ onSaveMilestone: PT.func,
+ onRemoveMilestone: PT.func,
+ projectMembers: PT.arrayOf(PT.shape()),
+ members: PT.object,
+ isUpdatable: PT.bool,
+}
+
+export default ManageMilestones
diff --git a/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.scss b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.scss
new file mode 100644
index 000000000..3edd9d897
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ManageMilestones/ManageMilestones.scss
@@ -0,0 +1,141 @@
+@import '~styles/variables';
+
+.toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 15px 0 20px;
+ min-height: 65px;
+
+ .primary-button {
+ margin-left: 12px;
+ }
+
+ .icon-button {
+ padding: 0 10px;
+ border: 0;
+ }
+
+ .separator {
+ width: 1px;
+ height: 15px;
+ margin-left: 20px;
+ background-color: $tc-gray-50;
+ }
+}
+
+.view-mode {
+ &.active {
+ svg path {
+ fill: $tc-green-110;
+ }
+ }
+}
+
+.table-container {
+ padding-left: 5px;
+ padding-right: 10px;
+ background-color: $tc-white;
+ border-radius: 3 * $corner-radius;
+}
+
+.milestones-table {
+ width: 100%;
+
+ th,
+ td {
+ text-align: left;
+ vertical-align: middle;
+
+ // checkbox
+ &:nth-child(1) {}
+ // milestone
+ &:nth-child(2) { padding-left: 5px; }
+ // description
+ &:nth-child(3) { padding-left: 15px; }
+ // start date
+ &:nth-child(4) { padding-left: 25px; }
+ // end date
+ &:nth-child(5) { padding-left: 15px; }
+ // status
+ &:nth-child(6) { padding-left: 20px; }
+ // budget
+ &:nth-child(7) {
+ padding-left: 20px;
+ :global(.milestone-budget-prefix-icon) { left: 25px; }
+ }
+ // copilot
+ &:nth-child(8) { padding-left: 25px; }
+ }
+
+ :global(.edit-milestone-row) {
+ th,
+ td {
+ // description
+ &:nth-child(3) { padding-left: 10px; }
+ // end date
+ &:nth-child(5) { padding-left: 10px; }
+ }
+ }
+
+ @media screen and (max-width: 1024px - 1px) {
+ th,
+ td {
+ // description
+ &:nth-child(3) { padding-left: 5px; }
+ // start date
+ &:nth-child(4) { padding-left: 10px; }
+ // end date
+ &:nth-child(5) { padding-left: 5px; }
+ // status
+ &:nth-child(6) { padding-left: 10px; }
+ // budget
+ &:nth-child(7) {
+ padding-left: 10px;
+ :global {
+ .milestone-budget-prefix-icon {
+ left: 15px;
+ }
+ .tc-file-field__inputs {
+ min-width: 64px;
+ padding-left: 10px;
+ }
+ }
+ }
+ // copilot
+ &:nth-child(8) { padding-left: 15px; }
+ }
+
+ :global(.edit-milestone-row) {
+ th,
+ td {
+ // description
+ &:nth-child(3) { padding-left: 5px; }
+ // end date
+ &:nth-child(5) { padding-left: 5px; }
+ }
+ }
+ }
+
+ td {
+ padding: 10px 0;
+ border-top: 1px solid rgba($tc-gray-60, .64);
+ }
+
+ :global {
+ .checkbox-group-item {
+ margin: 0;
+ transform: scale(60%);
+
+ > .tc-checkbox {
+ > label {
+ border-radius: 3 * $corner-radius;
+ }
+ }
+
+ > label {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/ManageMilestones/index.js b/src/projects/detail/components/SimplePlan/ManageMilestones/index.js
new file mode 100644
index 000000000..15f4886ad
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ManageMilestones/index.js
@@ -0,0 +1,2 @@
+import ManageMilestones from './ManageMilestones'
+export default ManageMilestones
diff --git a/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.jsx b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.jsx
new file mode 100644
index 000000000..9a30589bd
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.jsx
@@ -0,0 +1,90 @@
+/**
+ * Show project details
+ */
+import React from 'react'
+import PT from 'prop-types'
+import moment from 'moment'
+import * as constants from '../../../../../config/constants'
+import ProjectProgress from '../../../../../components/ProjectProgress/ProjectProgress'
+import ProjectMemeberAvatars from '../components/ProjectMemberAvatars'
+
+import './ProjectDetailsWidget.scss'
+
+
+function ProjectDetailsWidget({ project, phases }) {
+ const startDate = moment.min(phases.map(phase => moment(phase.startDate)))
+ const endDate = moment.max(phases.map(phase => moment(phase.endDate)))
+ const budget = phases.reduce((sum, phase) => sum + phase.budget, 0)
+ const spent = phases.reduce((sum, phase) => sum + phase.spentBudget, 0)
+ const numCompleted = phases.filter(phase => phase.status === constants.PHASE_STATUS_COMPLETED).length
+ const numTotal = phases.length
+
+ return (
+
+
+ {project.name}
+
+
+
+
+
+
+
+
Start Date
+
{startDate.format('YYYY-MM-DD')}
+
+
+
End Date
+
{endDate.format('YYYY-MM-DD')}
+
+
+
+
Progress
+
+
+
{`${numCompleted}/${numTotal}`}
+
+
+
+
Budget
+
+
+
{`$${formatBudget(budget)}`}
+
+
+
+
+
+ )
+}
+
+ProjectDetailsWidget.propTypes = {
+ project: PT.shape(),
+ phases: PT.arrayOf(PT.shape())
+}
+
+export default ProjectDetailsWidget
+
+/*
+ * Returns formatted text without rounding
+ * 100.9 -> '$100'
+ * 1100.9 -> '1.1K'
+ * 111900.9 -> '111K'
+ */
+function formatBudget(value) {
+ if (value < 100000) {
+ return value < 1000
+ ? `${Math.floor(value)}`
+ : `${Math.floor(value / 1000 * 10) / 10}K`
+ }
+ return `${Math.floor(value / 1000)}K`
+}
diff --git a/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.scss b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.scss
new file mode 100644
index 000000000..5aaa52195
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/ProjectDetailsWidget.scss
@@ -0,0 +1,114 @@
+@import '~styles/variables';
+
+.project-details-widget {
+ padding: 20px 25px;
+ font-size: $tc-body-md;
+ background-color: $tc-white;
+ border-radius: 3 * $corner-radius;
+
+ .title {
+ @include roboto-medium;
+
+ padding-bottom: 20px;
+ font-size: $tc-heading-md;
+ border-bottom: 2px solid $tc-gray-05;
+ }
+
+ .body {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 15px;
+
+ @media screen and (max-width: 1024px - 1px) {
+ flex-wrap: wrap;
+ }
+ }
+
+ .detail-item {
+ margin-right: 25px;
+
+ &:last-child {
+ margin-right: 0px;
+ }
+
+ &.description {
+ width: 43%;
+ line-height: 1.5;
+
+ @media screen and (max-width: 1024px - 1px) {
+ width: 100%;
+ margin-right: 0;
+ margin-bottom: 15px;
+ }
+ }
+ &.dates {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-width: 75px;
+ max-height: 70px;
+
+ .caption {
+ margin-bottom: 2px;
+ }
+ }
+ &.progress,
+ &.budget,
+ &.team {
+ min-width: 50px;
+ }
+
+ &.description,
+ &.dates,
+ &.progress,
+ &.budget {
+ flex: none;
+ }
+ &.team {
+ flex: 1 1 auto;
+ }
+ }
+
+ .caption {
+ @include roboto-bold;
+ font-size: $tc-label-md;
+ display: block;
+ margin-bottom: 10px;
+ }
+
+ .text {
+ font-size: $tc-label-sm;
+ }
+
+ .project-progress,
+ .project-budget {
+ position: relative;
+ width: 54px;
+ height: 54px;
+
+ .value {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ @include roboto-bold;
+ font-size: $tc-label-md;
+ }
+ }
+
+ :global {
+ .CircularProgressbar {
+ width: 100%;
+ height: 100%;
+
+ .CircularProgressbar-trail {
+ stroke-width: 10px;
+ }
+
+ .CircularProgressbar-path {
+ stroke-width: 10px;
+ }
+ }
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/index.js b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/index.js
new file mode 100644
index 000000000..3f28a5a86
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/ProjectDetailsWidget/index.js
@@ -0,0 +1,2 @@
+import ProjectDetailsWidget from './ProjectDetailsWidget'
+export default ProjectDetailsWidget
diff --git a/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.jsx b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.jsx
new file mode 100644
index 000000000..5720b20fe
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.jsx
@@ -0,0 +1,111 @@
+import React from 'react'
+import PT from 'prop-types'
+import uncontrollable from 'uncontrollable'
+import { PROJECT_ROLE_COPILOT } from '../../../../../../config/constants'
+import { PERMISSIONS } from '../../../../../../config/permissions'
+import { hasPermission } from '../../../../../../helpers/permissions'
+import Select from '../../../../../../components/Select/Select'
+import Avatar from 'appirio-tech-react-components/components/Avatar/Avatar'
+import {getAvatarResized, getFullNameWithFallback} from '../../../../../../helpers/tcHelpers'
+import IconXMark from '../../../../../../assets/icons/x-mark-thin.svg'
+
+import './AddCopilotsSidebar.scss'
+
+function AddCopilotsSidebar({
+ memberToAdd,
+ setMemberToAdd,
+ copilots,
+ projectMembers,
+ onClose,
+ onAdd,
+ onRemove
+}) {
+ const canManageCopilots = hasPermission(PERMISSIONS.MANAGE_COPILOTS)
+ const canRemoveCopilots = hasPermission(PERMISSIONS.REMOVE_COPILOTS)
+
+ const projectMemberOptions = projectMembers.map(projectMember => ({
+ label: projectMember.handle,
+ value: projectMember
+ })).filter(option => copilots.indexOf(option.value) === -1 && option.value.role === PROJECT_ROLE_COPILOT)
+
+
+ return (
+
+
+ Copilot
+
+
+
+
+
+ Add New Copilot
+ {
+ if (!selectedOption) {
+ return
+ }
+
+ setMemberToAdd(selectedOption.value)
+ }}
+ value={projectMemberOptions.find(option => option.value === memberToAdd) || null}
+ placeholder="- Select -"
+ isClearable={false}
+ />
+ {
+ onAdd(memberToAdd)
+ setMemberToAdd(null)
+ }}
+ disabled={!memberToAdd}
+ styleName="add-button"
+ >
+ ADD
+
+
+
+ {copilots.map((member, index) => (
+
+
+
+
+ {getFullNameWithFallback(member)}
+
+ @{member.handle || 'ConnectUser'}
+
+
+ {(canManageCopilots || canRemoveCopilots) && (
+
{
+ onRemove(member)
+ }}
+ >
+ ×
+
+ )}
+
+
+ ))}
+
+
+ )
+}
+
+AddCopilotsSidebar.propTypes = {
+ copilots: PT.arrayOf(PT.shape()),
+ projectMembers: PT.arrayOf(PT.shape()),
+ onClose: PT.func,
+ onAdd: PT.func,
+ onRemove: PT.func,
+}
+
+export default uncontrollable(AddCopilotsSidebar, {
+ memberToAdd: 'setMemberToAdd'
+})
diff --git a/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.scss b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.scss
new file mode 100644
index 000000000..4407feb0b
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/AddCopilotsSidebar.scss
@@ -0,0 +1,99 @@
+@import '~styles/variables';
+
+.button-close {
+ padding: 0 10px;
+ margin-left: auto;
+ margin-right: -10px;
+ border: 0;
+}
+
+.add-copilots-sidebar {
+ width: 100%;
+ height: 100%;
+ color: $tc-gray-90;
+ background-color: $tc-white;
+ box-shadow: 0 3px 12px rgba(#000, .16);
+
+ .title,
+ .select-copilot,
+ .copilot-list {
+ padding: 10px 15px 10px 25px;
+ }
+
+ .title {
+ @include roboto-bold;
+
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ line-height: 22px;
+ }
+
+ .select-copilot {
+ border-top: 1px solid $tc-gray-05;
+
+
+ .label {
+ display: block;
+ margin-bottom: 7px;
+ font-size: $tc-label-md;
+ line-height: 16px;
+ }
+
+ .add-button {
+ margin-top: 15px;
+ }
+
+ :global {
+ .react-select__control {
+ @include roboto-light;
+ font-size: $tc-body-sm;
+
+ .react-select__indicators,
+ .react-select__indicator-separator,
+ .react-select__dropdown-indicator {
+ display: flex;
+ }
+ .react-select__clear-indicator {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .copilot-list {
+ padding-top: 25px;
+
+ &:not(:empty) {
+ border-top: 1px solid $tc-gray-05;
+ }
+ }
+}
+
+.member-details {
+ display: flex;
+ align-items: center;
+ padding: 5px 0;
+ color: $tc-gray-90;
+
+ .name-and-handle {
+ margin-left: 10px;
+ font-size: $tc-body-sm;
+
+ .fullname {
+ font-weight: bold;
+ }
+
+ .handle {
+ &::before {
+ content: ' ';
+ }
+ }
+ }
+
+ .close-button {
+ padding: 0 10px;
+ margin-left: auto;
+ color: inherit;
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/index.js b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/index.js
new file mode 100644
index 000000000..0dfc593b3
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/AddCopilotsSidebar/index.js
@@ -0,0 +1,2 @@
+import AddCopilotsSidebar from './AddCopilotsSidebar'
+export default AddCopilotsSidebar
diff --git a/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.jsx b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.jsx
new file mode 100644
index 000000000..4263425bb
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.jsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import PT from 'prop-types'
+import IconHelp from '../../../../../../assets/icons/help-me.svg'
+
+import './ConfirmDeleteMilestone.scss'
+
+function ConfirmDeleteMilestone({ onClose }) {
+ return (
+
+
+
+ Deletion Confirmation
+
+
+ Are you sure you want to delete the selected Milestone (s)?
+
+
+ onClose(true)}>YES
+ onClose()}>NO
+
+
+ )
+}
+
+ConfirmDeleteMilestone.propTypes = {
+ onClose: PT.func,
+}
+
+export default ConfirmDeleteMilestone
diff --git a/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.scss b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.scss
new file mode 100644
index 000000000..c7d7beb8c
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/ConfirmDeleteMilestone.scss
@@ -0,0 +1,48 @@
+@import '~styles/variables';
+
+.confirm-delete-milestone {
+ width: 204px;
+ padding: 15px 8px 12px 12px;
+ color: $tc-black;
+ background-color: $tc-white;
+ border: 1px solid $tc-gray-50;
+ border-radius: 3 * $corner-radius;
+ box-shadow: 0 3px 6px rbga(#000, .16);
+
+ .icon {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ color: red;
+ stroke: currentColor;
+
+ * {
+ stroke: inherit;
+ }
+ }
+
+ .title {
+ @include roboto-bold;
+
+ display: flex;
+ margin-bottom: 8px;
+ font-size: $tc-body-xs;
+ line-height: 15px;
+ }
+
+ .text {
+ @include roboto;
+
+ margin-bottom: 12px;
+ font-size: $tc-body-sm;
+ line-height: 16px;
+ }
+
+ .footer {
+ button + button {
+ margin-left: 10px;
+ }
+ }
+}
+
+
diff --git a/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/index.js b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/index.js
new file mode 100644
index 000000000..5d55d86a4
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ConfirmDeleteMilestone/index.js
@@ -0,0 +1,2 @@
+import ConfirmDeleteMilestone from './ConfirmDeleteMilestone'
+export default ConfirmDeleteMilestone
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx
new file mode 100644
index 000000000..64bea9532
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.jsx
@@ -0,0 +1,25 @@
+/**
+ * Milestone budget
+ */
+import React from 'react'
+import PT from 'prop-types'
+
+import './MilestoneBudget.scss'
+
+function MilestoneBudget({ spent, budget }) {
+ return (
+
+
{budget > 0 ? `$${spent} of $${budget}` : `$${budget}`}
+
+
0 ? spent/budget : 0})`}}/>
+
+ )
+}
+
+
+MilestoneBudget.propTypes = {
+ spent: PT.number,
+ budget: PT.number,
+}
+
+export default MilestoneBudget
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss
new file mode 100644
index 000000000..308256d79
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/MilestoneBudget.scss
@@ -0,0 +1,35 @@
+@import '~styles/variables';
+
+.progress-bar {
+ position: relative;
+ padding-bottom: 6px;
+
+ .text {
+ @include roboto;
+ font-size: 9px;
+ line-height: 12px;
+ color: $tc-gray-20;
+ }
+
+ .background,
+ .progress {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ border-width: 3px;
+ border-style: solid;
+ border-radius: 6px;
+ }
+
+ .background {
+ background-color: $tc-gray-20;
+ border-color: $tc-gray-20;
+ }
+
+ .progress {
+ background-color: $tc-green-110;
+ border-color: $tc-green-110;
+ transform-origin: left;
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js
new file mode 100644
index 000000000..d76b13aa7
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneBudget/index.js
@@ -0,0 +1,2 @@
+import MilestoneBudget from './MilestoneBudget'
+export default MilestoneBudget
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx
new file mode 100644
index 000000000..30d39b0f4
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.jsx
@@ -0,0 +1,75 @@
+/**
+ * Milestone copilots
+ */
+import React from 'react'
+import PT from 'prop-types'
+import uncontrollable from 'uncontrollable'
+import { Gateway } from 'react-gateway'
+import ProjectManagerAvatars from '../../../../../list/components/Projects/ProjectManagerAvatars'
+import AddCopilotsSidebar from '../AddCopilotsSidebar'
+import IconDots from '../../../../../../assets/icons/icon-dots.svg'
+
+import './MilestoneCopilots.scss'
+
+
+function MilestoneCopilots({
+ open,
+ setOpen,
+ edit,
+ copilots,
+ projectMembers,
+ onAdd,
+ onRemove
+}) {
+ const ScrollLock = React.createClass ({
+ componentDidMount() {
+ const scrollbarWidth = window.innerWidth - document.body.clientWidth
+ document.body.style.setProperty('overflow', 'hidden')
+ document.body.style.setProperty('margin-right', `${scrollbarWidth}px`)
+ },
+
+ componentWillUnmount() {
+ document.body.style.removeProperty('overflow')
+ document.body.style.removeProperty('margin-right')
+ },
+
+ render() {
+ return null
+ }
+ })
+
+ return edit ? (
+
+ setOpen(!open)}>
+
+
+ {open && (
+
+
+ setOpen(false)}
+ onAdd={onAdd}
+ onRemove={onRemove}
+ />
+
+ )}
+
+ ) : (
+
+ )
+}
+
+
+MilestoneCopilots.propTypes = {
+ edit: PT.bool,
+ copilots: PT.arrayOf(PT.shape()),
+ projectMembers: PT.arrayOf(PT.shape()),
+ onAdd: PT.func,
+ onRemove: PT.func,
+}
+
+export default uncontrollable(MilestoneCopilots, {
+ open: 'setOpen'
+})
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.scss b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.scss
new file mode 100644
index 000000000..5a609a145
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/MilestoneCopilots.scss
@@ -0,0 +1,9 @@
+@import '~styles/variables';
+
+.milestone-copilots {
+ .edit-copilots-button {
+ height: 25px;
+ padding: 0 8px;
+ border-radius: 2 * $corner-radius;
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/index.js
new file mode 100644
index 000000000..5b916ba07
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneCopilots/index.js
@@ -0,0 +1,2 @@
+import MilestoneCopilots from './MilestoneCopilots'
+export default MilestoneCopilots
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.jsx
new file mode 100644
index 000000000..42f32ff59
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.jsx
@@ -0,0 +1,89 @@
+import React from 'react'
+import PT from 'prop-types'
+import { Popper, Manager } from 'react-popper'
+import ConfirmDeleteMilestone from '../ConfirmDeleteMilestone'
+import IconTrash from '../../../../../../assets/icons/icon-ui-trash-simple.svg'
+
+import './MilestoneDeleteButton.scss'
+
+class MilestoneDeleteButton extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ open: false
+ }
+
+ this.onClickOutside = this.onClickOutside.bind(this)
+ }
+
+ componentDidUpdate() {
+ const { open } = this.state
+
+ if (open) {
+ document.addEventListener('click', this.onClickOutside)
+ } else {
+ document.removeEventListener('click', this.onClickOutside)
+ }
+ }
+
+ onClickOutside(event) {
+ if (this.confirmRef.contains(event.target)) {
+ return
+ }
+
+ this.setState({ open: false })
+ }
+
+ render() {
+ const { onDelete } = this.props
+ const { open } = this.state
+
+ return (
+
+ {
+ event.stopPropagation()
+
+ this.setState({
+ open: !open
+ })
+ }}
+ ref={ref => this.btnRef = ref}
+ >
+
+
+ {open &&
+
+
+ {({ ref, style, placement, arrowProps }) => (
+
+
this.confirmRef = ref2}>
+
{
+ if (yes) {
+ onDelete()
+ }
+ this.setState({ open: false })
+ }}
+ />
+
+
+
+ )}
+
+
+ }
+
+ )
+ }
+}
+
+MilestoneDeleteButton.propTypes = {
+ onDelete: PT.func,
+}
+
+export default MilestoneDeleteButton
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.scss b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.scss
new file mode 100644
index 000000000..41db3db24
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/MilestoneDeleteButton.scss
@@ -0,0 +1,16 @@
+button.icon-button{
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: 0;
+}
+
+.pane {
+ margin-top: 10px;
+ z-index: 1;
+}
+
+.arrow {
+ width: 0;
+ height: 0;
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/index.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/index.jsx
new file mode 100644
index 000000000..40e3ad63e
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneDeleteButton/index.jsx
@@ -0,0 +1,2 @@
+import MilestoneDeleteButton from './MilestoneDeleteButton'
+export default MilestoneDeleteButton
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx
new file mode 100644
index 000000000..6822d19b1
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.jsx
@@ -0,0 +1,59 @@
+/**
+ * Milestone header row
+ */
+import React from 'react'
+import PT from 'prop-types'
+import FormsyForm from 'appirio-tech-react-components/components/Formsy'
+
+import './MilestoneHeaderRow.scss'
+
+const TCFormFields = FormsyForm.Fields
+
+function MilestoneHeaderRow ({ milestones, onChangeMilestones, isUpdatable }) {
+ const checked = milestones.reduce(
+ (selected, milestone) => selected = selected && milestone.selected,
+ milestones.length > 0
+ )
+ const selectAll = () => {
+ const milestonesSelected = milestones.map(milestone => ({
+ ...milestone,
+ selected: true
+ }))
+ onChangeMilestones(milestonesSelected)
+ }
+ const unselectAll = () => {
+ const milestonesUnselected = milestones.map(milestone => ({
+ ...milestone,
+ selected: false
+ }))
+ onChangeMilestones(milestonesUnselected)
+ }
+
+ return (
+
+
+ {
+ value ? selectAll() : unselectAll()
+ }}
+ />
+
+ MILESTONE
+ DESCRIPTION
+ START DATE
+ END DATE
+ STATUS
+ BUDGET
+ {/* COPILOTS */}
+ {isUpdatable && (ACTION )}
+
+ )
+}
+
+MilestoneHeaderRow.propTypes = {
+ onChangeMilestones: PT.func,
+}
+
+export default MilestoneHeaderRow
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.scss b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.scss
new file mode 100644
index 000000000..3a059d1ae
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/MilestoneHeaderRow.scss
@@ -0,0 +1,14 @@
+@import '~styles/variables';
+
+.milestone-row {
+ @include roboto-bold;
+
+ font-size: $tc-label-sm;
+ color: $tc-gray-60;
+ line-height: 15px;
+
+ th {
+ padding-top: 20px;
+ padding-bottom: 15px;
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/index.js
new file mode 100644
index 000000000..8601aca13
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneHeaderRow/index.js
@@ -0,0 +1,2 @@
+import MilestoneHeaderRow from './MilestoneHeaderRow'
+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
new file mode 100644
index 000000000..4c7a5bd26
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.jsx
@@ -0,0 +1,358 @@
+/**
+ * View / edit milestone record
+ */
+import React from 'react'
+import PT from 'prop-types'
+import moment from 'moment'
+import FormsyForm from 'appirio-tech-react-components/components/Formsy'
+import _ from 'lodash'
+import { components } from 'react-select'
+import { isValidStartEndDates } from '../../../../../../helpers/utils'
+import FormsySelect from '../../../../../../components/Select/FormsySelect'
+// import MilestoneCopilots from '../MilestoneCopilots'
+import MilestoneStatus from '../MilestoneStatus'
+import MilestoneBudget from '../MilestoneBudget'
+import MilestoneDeleteButton from '../MilestoneDeleteButton'
+import { PHASE_STATUS_OPTIONS } from '../../../../../../config/constants'
+import IconCheck from '../../../../../../assets/icons/icon-check-thin.svg'
+import IconXMark from '../../../../../../assets/icons/icon-x-mark-thin.svg'
+import IconPencil from '../../../../../../assets/icons/icon-ui-pencil.svg'
+import IconDots from '../../../../../../assets/icons/icon-dots.svg'
+import IconArrowDown from '../../../../../../assets/icons/arrow-6px-carret-down-normal.svg'
+
+import styles from './MilestoneRow.scss'
+
+const TCFormFields = FormsyForm.Fields
+
+function MilestoneRow({
+ milestone,
+ rowId,
+ onChange,
+ onSave,
+ onRemove,
+ onDiscard,
+ projectMembers,
+ allMilestones,
+ isCreatingRow,
+ isUpdatable,
+ members,
+}) {
+ const phaseStatusOptions = PHASE_STATUS_OPTIONS
+ const edit = milestone.edit
+ const copilotIds = _.get(milestone, 'details.copilots', [])
+ let copilots = copilotIds.map(userId => projectMembers.find(member => member.userId === userId)).filter(Boolean)
+
+ if (copilots.length !== copilotIds.length) {
+ const missingCopilotIds = _.difference(copilotIds, projectMembers.map(member => member.userId))
+ const missingCopilots = missingCopilotIds.map(userId => members[userId])
+ copilots = copilots.concat(missingCopilots)
+ }
+
+ let milestoneRef
+ let startDateRef
+ let endDateRef
+ let budgetRef
+
+ return edit ? (
+
+
+ {
+ onChange({ ...milestone, selected: value })
+ }}
+ />
+
+
+ i.id !== milestone.id)
+ .map(i => i.name.toLowerCase().trim())
+ const inputtingTitle = values[`title-${rowId}`].toLowerCase().trim()
+ return existingTitles.indexOf(inputtingTitle) === -1
+ }
+ }}
+ validationError={'Please, enter name'}
+ validationErrors={{
+ checkDuplicatedTitles: 'Milestone name already exists'
+ }}
+ required
+ type="text"
+ name={`title-${rowId}`}
+ value={milestone.name || ''}
+ maxLength={48}
+ onChange={(_, value) => {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone }
+ }
+ onChange({...milestone, name: value, editting: true, editted: true })
+ }}
+ wrapperClass={styles.textInput}
+ innerRef={ref => milestoneRef = ref}
+ isPristine={() => !milestone.editted}
+ />
+
+
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ onChange({...milestone, description: value })
+ }}
+ wrapperClass={styles.textArea}
+ autoResize={false}
+ rows={1}
+ />
+
+
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ onChange({...milestone, startDate: value })
+ }}
+ wrapperClass={`${styles.textInput} ${styles.dateInput}`}
+ innerRef={ref => startDateRef = ref}
+ />
+
+
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ onChange({...milestone, endDate: value })
+ }}
+ wrapperClass={`${styles.textInput} ${styles.dateInput}`}
+ innerRef={ref => endDateRef = ref}
+ />
+
+
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ onChange({...milestone, status: selectedOption.value })
+ }}
+ value={phaseStatusOptions.find(option => option.value === milestone.status)}
+ isSearchable={false}
+ components={{
+ DropdownIndicator: (props) => (
+
+
+
+ )
+ }}
+ />
+
+
+ $
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ onChange({...milestone, budget: value })
+ }}
+ wrapperClass={styles.textInput}
+ innerRef={ref => budgetRef = ref}
+ />
+
+ {/*
+ {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ const details = milestone.details
+ const copilotIdsUpdated = copilots.map(copilot => copilot.userId).concat(member.userId)
+ onChange({...milestone, details: { ...details, copilots: copilotIdsUpdated } })
+ }}
+ onRemove={(member) => {
+ if (!milestone.origin) {
+ milestone.origin = {...milestone}
+ }
+ const details = milestone.details
+ const copilotIdsUpdated = copilots.filter(copilot => copilot.userId !== member.userId).map(copilot => copilot.userId)
+ onChange({...milestone, details: { ...details, copilots: copilotIdsUpdated } })
+ }}
+ />
+ */}
+
+
+ {
+ milestone.editted = true
+ milestone.editting = true
+ if (milestoneRef.props.isValid()
+ && startDateRef.props.isValid()
+ && endDateRef.props.isValid()
+ && budgetRef.props.isValid()
+ ) {
+ onSave(milestone.id)
+ }
+ }}
+ >
+
+
+ {
+ if (isCreatingRow) {
+ onDiscard(milestone.id)
+ } else {
+ onChange({ ...(milestone.origin || milestone), edit: false })
+ }
+ }}
+ >
+
+
+
+
+
+ ) : (
+
+
+ {
+ onChange({ ...milestone, selected: value })
+ }}
+ />
+
+
+ {milestone.name}
+
+
+ {milestone.description}
+
+
+ {moment(milestone.startDate).format('MM-DD-YYYY')}
+
+
+ {moment(milestone.endDate).format('MM-DD-YYYY')}
+
+
+
+
+
+
+
+ {/*
+
+ */}
+ {isUpdatable && (
+
+
+ {
+ onChange({ ...milestone, edit: true })
+ }}
+ >
+
+
+ {
+ onRemove(milestone.id)
+ }}
+ />
+
+
+
+
+
+ )}
+
+ )
+}
+
+MilestoneRow.propTypes = {
+ milestone: PT.shape(),
+ rowId: PT.string,
+ onChange: PT.func,
+ onSave: PT.func,
+ onRemove: PT.func,
+ onDiscard: PT.func,
+ projectMembers: PT.arrayOf(PT.shape()),
+ allMilestones: PT.arrayOf(PT.shape()),
+ isCreatingRow: PT.bool,
+ isUpdatable: PT.bool,
+ members: PT.object,
+}
+
+export default MilestoneRow
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.scss b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.scss
new file mode 100644
index 000000000..ce1e1f152
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneRow/MilestoneRow.scss
@@ -0,0 +1,170 @@
+@import '~styles/variables';
+
+.milestone-row {
+ @include roboto;
+ font-size: $tc-body-sm;
+ color: $tc-gray-60;
+
+ .icon-button{
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: 0;
+ }
+
+ .checkbox {}
+
+ .milestone {}
+
+ .description {}
+
+ .start-date {
+ white-space: nowrap;
+ }
+
+ .end-date {
+ white-space: nowrap;
+ }
+
+ .status {
+ white-space: nowrap;
+ }
+
+ .budget {
+ padding-right: 10px;
+ }
+
+ .copilots {}
+
+ .action {}
+}
+
+.textInput {
+ position: relative;
+
+ :global {
+ .tc-label {
+ display: none;
+ }
+ .tc-file-field__inputs {
+ height: 25px;
+ margin-bottom: 0;
+ font-size: $tc-body-sm;
+ line-height: 16px;
+ }
+ .error-message {
+ position: absolute;
+ top: 100%;
+ width: fit-content;
+ min-width: 145px;
+ z-index: 1;
+ }
+ }
+}
+
+.textArea {
+ :global {
+ .tc-label {
+ display: none;
+ }
+ .tc-textarea {
+ height: 25px;
+ min-height: 25px;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin: 0;
+ overflow: hidden;
+ resize: vertical;
+ font-size: $tc-body-sm;
+ line-height: 25px;
+ }
+ }
+}
+
+.dateInput {
+ :global {
+ .tc-file-field__inputs {
+ padding: 0;
+ max-width: 120px;
+
+ &::-webkit-calendar-picker-indicator {
+ width: 16px;
+ margin: 0;
+ }
+
+ @media screen and (max-width: 1024px) {
+ max-width: 90px;
+ }
+ }
+ }
+}
+
+.status {
+ :global {
+ .react-select-hiddendropdown-container {
+ .react-select__control {
+ height: auto;
+ min-height: 25px;
+ overflow: hidden;
+
+ .react-select__value-container {
+ height: auto;
+ padding-top: 0;
+ padding-bottom: 0;
+
+ input {
+ margin-bottom: 0;
+ }
+ }
+ .react-select__single-value {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ .react-select__indicators {
+ display: flex;
+ height: auto;
+ }
+ .react-select__clear-indicator {
+ display: none;
+ }
+ .react-select__indicator-separator {
+ display: none;
+ }
+ .react-select__dropdown-indicator {
+ display: flex;
+ }
+ }
+ .react-select__menu {
+ min-width: 100px;
+ }
+ }
+ }
+}
+
+.budget {
+ position: relative;
+
+ .prefix-icon {
+ position: absolute;
+ left: 0;
+ top: calc(50% - 6px);
+ z-index: 1;
+ }
+
+ :global {
+ .tc-file-field__inputs {
+ padding-right: 0;
+ padding-left: 20px;
+ }
+ }
+}
+
+.action {
+ .inline-menu {
+ display: flex;
+
+ > *:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneRow/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneRow/index.js
new file mode 100644
index 000000000..13cdadc92
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneRow/index.js
@@ -0,0 +1,2 @@
+import MilestoneRow from './MilestoneRow'
+export default MilestoneRow
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.jsx b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.jsx
new file mode 100644
index 000000000..cceb22325
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.jsx
@@ -0,0 +1,22 @@
+/**
+ * Milestone status
+ */
+import React from 'react'
+import PT from 'prop-types'
+import { PHASE_STATUS_OPTIONS } from '../../../../../../config/constants'
+import './MilestoneStatus.scss'
+
+function MilestoneStatus({ status }) {
+ const label = PHASE_STATUS_OPTIONS.find(option => option.value === status).label
+ return (
+
+ {label}
+
+ )
+}
+
+MilestoneStatus.propTypes = {
+ status: PT.string,
+}
+
+export default MilestoneStatus
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.scss b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.scss
new file mode 100644
index 000000000..751538f03
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/MilestoneStatus.scss
@@ -0,0 +1,20 @@
+@import '~styles/variables';
+
+.milestone-status {
+ @include roboto-bold;
+
+ display: inline-block;
+ padding: 4px 15px;
+ color: $tc-white;
+ font-size: $tc-label-xxs;
+ line-height: 16px;
+ border-radius: 20px;
+
+ &.draft { background-color: $tc-gray-20; }
+ &.in_review { background-color: $tc-gray-20; }
+ &.reviewed { background-color: $tc-gray-20; }
+ &.active { background-color: $tc-green-100; }
+ &.completed { background-color: $tc-black; }
+ &.cancelled { background-color: $tc-black; }
+ &.paused { background-color: $tc-red-100; }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/MilestoneStatus/index.js b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/index.js
new file mode 100644
index 000000000..882049bc5
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/MilestoneStatus/index.js
@@ -0,0 +1,3 @@
+import MilestoneStatus from './MilestoneStatus'
+export default MilestoneStatus
+
diff --git a/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.jsx b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.jsx
new file mode 100644
index 000000000..95af711af
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.jsx
@@ -0,0 +1,63 @@
+import React from 'react'
+import PT from 'prop-types'
+import ProjectManagerAvatars from '../../../../../list/components/Projects/ProjectManagerAvatars'
+
+import './ProjectMemberAvatars.scss'
+
+class ProjectMemberAvatars extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ shownNum: null
+ }
+ }
+
+ componentDidMount() {
+ this.resizeObserver = new ResizeObserver(entries => {
+ for (const entry of entries) {
+ if (entry.borderBoxSize) {
+ const borderBoxSize = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0]: entry.borderBoxSize
+
+ const numAvatarsToShow = Math.floor(borderBoxSize.inlineSize / this.props.size)
+ if (numAvatarsToShow !== this.props.maxShownNum) {
+ this.setState({ shownNum: numAvatarsToShow })
+ }
+ }
+ }
+ })
+ this.resizeObserver.observe(this.ref)
+ }
+
+ componentWillUnmount() {
+ this.resizeObserver.unobserve(this.ref)
+ }
+
+ render() {
+ const { members, maxShownNum, size } = this.props
+ const { shownNum } = this.state
+
+ const numAvatarsToShow = shownNum || maxShownNum
+
+ return (
+
this.ref = ref}
+ styleName={`project-member-avatars size-${size}`}
+ >
+
+
+ )
+ }
+}
+
+ProjectMemberAvatars.propTypes = {
+ members: PT.arrayOf(PT.shape()),
+ maxShownNum: PT.number,
+ size: PT.number,
+}
+
+export default ProjectMemberAvatars
diff --git a/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.scss b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.scss
new file mode 100644
index 000000000..fc8deed85
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/ProjectMemberAvatars.scss
@@ -0,0 +1,69 @@
+@import '~styles/variables';
+
+$avatar-overlap: 5px;
+
+.project-member-avatars {
+ :global {
+ .plus-user {
+ top: 0;
+ left: auto;
+ right: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+
+ @include roboto-bold;
+ color: $tc-white;
+ }
+ .stack-avatar {
+ &:last-of-type {
+ .sb-avatar {
+ position: relative;
+
+ // mask
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(#000, .62);
+ border-radius: 50%;
+ }
+ }
+ }
+ &:last-child {
+ .sb-avatar {
+ // hide mask
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+ }
+
+ &.size-50 {
+ margin-right: -50px;
+
+ :global(.plus-user) {
+ right: 25px;
+ width: 50px;
+ height: 50px;
+ margin-left: -$avatar-overlap;
+ width: 0;
+ }
+ }
+ &.size-35 {
+ margin-right: -35px;
+
+ :global(.plus-user) {
+ right: 35px;
+ width: 35px;
+ height: 35px;
+ margin-left: -$avatar-overlap;
+ }
+ }
+}
diff --git a/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/index.js b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/index.js
new file mode 100644
index 000000000..d772198d8
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/ProjectMemberAvatars/index.js
@@ -0,0 +1,2 @@
+import ProjectMemberAvatars from './ProjectMemberAvatars'
+export default ProjectMemberAvatars
diff --git a/src/projects/detail/components/SimplePlan/components/helpers/milestone.js b/src/projects/detail/components/SimplePlan/components/helpers/milestone.js
new file mode 100644
index 000000000..b85b585d4
--- /dev/null
+++ b/src/projects/detail/components/SimplePlan/components/helpers/milestone.js
@@ -0,0 +1,18 @@
+import moment from 'moment'
+import * as constants from '../../../../../../config/constants'
+
+let nextId = 0
+export function createEmptyMilestone(startDate, endDate) {
+ startDate = startDate ? moment(startDate) : moment()
+ endDate = endDate ? moment(endDate) : moment(startDate).add(3, 'days')
+
+ return {
+ id: `new-milestone-${nextId++}`,
+ name: '',
+ description: '',
+ startDate: moment(startDate).format('YYYY-MM-DD'),
+ endDate: moment(endDate).format('YYYY-MM-DD'),
+ status: constants.PHASE_STATUS_DRAFT,
+ budget: 0,
+ }
+}
diff --git a/src/projects/detail/containers/DashboardContainer.jsx b/src/projects/detail/containers/DashboardContainer.jsx
index 329925369..17b7a8c68 100644
--- a/src/projects/detail/containers/DashboardContainer.jsx
+++ b/src/projects/detail/containers/DashboardContainer.jsx
@@ -10,6 +10,7 @@ import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator'
import CreatePhaseForm from '../components/CreatePhaseForm'
+import moment from 'moment'
import './DashboardContainer.scss'
@@ -46,9 +47,11 @@ import NotificationsReader from '../../../components/NotificationsReader'
import { hasPermission } from '../../../helpers/permissions'
import { getProjectTemplateById } from '../../../helpers/templates'
import { PERMISSIONS } from '../../../config/permissions'
-import { updateProject, fireProjectDirty, fireProjectDirtyUndo } from '../../actions/project'
+import { updateProject, fireProjectDirty, fireProjectDirtyUndo, updatePhase } from '../../actions/project'
import { addProjectAttachment, updateProjectAttachment, removeProjectAttachment } from '../../actions/projectAttachment'
+import { loadMembers } from '../../../actions/members'
import ProjectEstimation from '../../create/components/ProjectEstimation'
+import CreateSimplePlan from '../components/SimplePlan/CreateSimplePlan'
import {
PHASE_STATUS_ACTIVE,
@@ -79,6 +82,7 @@ class DashboardContainer extends React.Component {
this.state = {
open: false,
+ createGameplanPhases: null,
}
this.onNotificationRead = this.onNotificationRead.bind(this)
this.toggleDrawer = this.toggleDrawer.bind(this)
@@ -158,6 +162,8 @@ class DashboardContainer extends React.Component {
removeProjectAttachment,
location,
estimationQuestion,
+ loadMembers,
+ members,
} = this.props
const projectTemplate = project && project.templateId && projectTemplates ? (getProjectTemplateById(projectTemplates, project.templateId)) : null
@@ -275,23 +281,169 @@ class DashboardContainer extends React.Component {
productCategories={productCategories}
/>
- {visiblePhases && visiblePhases.length > 0 ? (
-
+ {project.version !== 'v4' ? (
+
+ {visiblePhases && visiblePhases.length > 0 ? (
+
+ ) : (
+
+ )}
+ {isCreatingPhase?
: null}
+ {isProjectLive && !isCreatingPhase && hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && !isLoadingPhases && (
+
+ )}
+
) : (
-
- )}
- {isCreatingPhase?
: null}
- {isProjectLive && !isCreatingPhase && hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && !isLoadingPhases && (
-
+
+ {((!hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && (!visiblePhases || visiblePhases.length === 0))
+ || (hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) &&
+ (!phases || phases.length === 0) &&
+ (!this.state.createGameplanPhases || this.state.createGameplanPhases.length === 0))
+ ) && (
+
+ )}
+ {(() => {
+ // is loading
+ if (isCreatingPhase || isLoadingPhases) {
+ return null
+ }
+
+ // hide milestones form if customer and no visible milestones
+ if (!hasPermission(PERMISSIONS.MANAGE_PROJECT_PLAN) && visiblePhases && visiblePhases.length === 0) {
+ return null
+ }
+
+ return (
+
{
+ this.setState({createGameplanPhases: milestones})
+ }}
+ onSaveMilestone={(id) => {
+ const { createGameplanPhases } = this.state
+ const index = createGameplanPhases.findIndex(phase => phase.id === id)
+ const phase = createGameplanPhases[index]
+
+ if (`${phase.id}`.startsWith('new-milestone')) {
+ const productTemplate = {
+ name: phase.name,
+ id: PHASE_PRODUCT_TEMPLATE_ID,
+ }
+
+ if (phase.description && phase.description.trim()) {
+ productTemplate.description = phase.description.trim()
+ }
+
+ this.props.createPhaseWithoutTimeline(
+ project,
+ productTemplate,
+ phase.status,
+ moment.utc(phase.startDate),
+ moment.utc(phase.endDate),
+ phase.budget,
+ phase.details
+ ).then(({ action }) => {
+ // reload phase
+ const updatedCreateGameplanPhases = [...createGameplanPhases]
+ updatedCreateGameplanPhases.splice(index, 1, {
+ ...action.payload.phase,
+ selected: phase.selected
+ })
+ this.setState({ createGameplanPhases: updatedCreateGameplanPhases })
+ })
+ } else {
+ const updateParam = {
+ name: phase.name,
+ startDate: moment.utc(phase.startDate),
+ endDate: moment.utc(phase.endDate),
+ status: phase.status,
+ budget: phase.budget,
+ }
+
+ if (phase.description && phase.description.trim()) {
+ updateParam.description = phase.description.trim()
+ }
+
+ if (phase.details) {
+ updateParam.details = phase.details
+ }
+
+ this.props.updatePhase(
+ phase.projectId,
+ phase.id,
+ updateParam
+ ).then(({ action }) => {
+ const updatedCreateGameplanPhases = [...this.state.createGameplanPhases]
+ const idx = updatedCreateGameplanPhases.findIndex(phase => phase.id === action.payload.id)
+
+ // reload phase
+ updatedCreateGameplanPhases.splice(idx, 1, {
+ ...action.payload,
+ edit: this.state.createGameplanPhases[idx].edit,
+ selected: this.state.createGameplanPhases[idx].selected,
+ })
+ this.setState({ createGameplanPhases: updatedCreateGameplanPhases })
+ })
+
+ // toggle edit
+ const updatedCreateGameplanPhases = [...this.state.createGameplanPhases]
+ updatedCreateGameplanPhases.splice(index, 1, {
+ ...phase,
+ edit: false,
+ selected: phase.selected
+ })
+ this.setState({ createGameplanPhases: updatedCreateGameplanPhases })
+ }
+ }}
+ onRemoveMilestone={(id) => {
+ const { createGameplanPhases } = this.state
+ let index
+ let projectId
+
+ if (createGameplanPhases) {
+ index = createGameplanPhases.findIndex(phase => phase.id === id)
+ projectId = createGameplanPhases[index].projectId
+ } else {
+ index = phases.findIndex(phase => phase.id === id)
+ projectId = phases[index].projectId
+ }
+
+ this.props.deleteProjectPhase(
+ projectId,
+ id
+ ).then(() => {
+ if (!this.state.createGameplanPhases) {
+ return
+ }
+
+ // remove phase
+ const index = this.state.createGameplanPhases.findIndex(phase => phase.id === id)
+ const newGameplanPhases = [...this.state.createGameplanPhases]
+ newGameplanPhases.splice(index, 1)
+ this.setState({createGameplanPhases: newGameplanPhases})
+ })
+ }}
+ />
+ )
+ })()}
+
)}
)}
@@ -301,7 +453,7 @@ class DashboardContainer extends React.Component {
}
}
-const mapStateToProps = ({ notifications, projectState, projectTopics, templates, topics }) => {
+const mapStateToProps = ({ notifications, projectState, projectTopics, templates, topics, members }) => {
// all feeds includes primary as well as private topics if user has access to private topics
let allFeed = projectTopics.feeds[PROJECT_FEED_TYPE_PRIMARY].topics
if (hasPermission(PERMISSIONS.ACCESS_PRIVATE_POST)) {
@@ -322,6 +474,7 @@ const mapStateToProps = ({ notifications, projectState, projectTopics, templates
isFeedsLoading: projectTopics.isLoading,
phasesStates: projectState.phasesStates,
phasesTopics: topics,
+ members: members.members
}
}
@@ -345,7 +498,9 @@ const mapDispatchToProps = {
updateProject,
addProjectAttachment,
updateProjectAttachment,
- removeProjectAttachment
+ removeProjectAttachment,
+ updatePhase,
+ loadMembers,
}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(DashboardContainer))
diff --git a/src/projects/detail/containers/DashboardContainer.scss b/src/projects/detail/containers/DashboardContainer.scss
index e69de29bb..8f844c4e1 100644
--- a/src/projects/detail/containers/DashboardContainer.scss
+++ b/src/projects/detail/containers/DashboardContainer.scss
@@ -0,0 +1,16 @@
+@import '~styles/variables';
+
+.simple-plan {
+ @media screen and (min-width: $screen-md) {
+ margin-top: 30px;
+ }
+
+ @media screen and (max-width: $screen-md - 1px) {
+ padding: 0 20px;
+ }
+}
+
+.welcome-message {
+ margin: 0 auto;
+ max-width: 760px;
+}
diff --git a/src/projects/list/components/Projects/ProjectManagerAvatars.jsx b/src/projects/list/components/Projects/ProjectManagerAvatars.jsx
index 5a1759aa5..4a14b9e69 100644
--- a/src/projects/list/components/Projects/ProjectManagerAvatars.jsx
+++ b/src/projects/list/components/Projects/ProjectManagerAvatars.jsx
@@ -5,7 +5,7 @@ import UserTooltip from '../../../../components/User/UserTooltip'
import styles from './ProjectManagerAvatars.scss'
-const ProjectManagerAvatars = ({ managers, maxShownNum = 3 }) => {
+const ProjectManagerAvatars = ({ managers, maxShownNum = 3, size = 35 }) => {
let extM = false
if (!managers || !managers.length)
return
Unclaimed
@@ -19,7 +19,7 @@ const ProjectManagerAvatars = ({ managers, maxShownNum = 3 }) => {
{uniqManagers.map((user, i) => {
return (
-
+
)
})}
diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js
index dbbca6a6f..9997dfe23 100644
--- a/src/reducers/alerts.js
+++ b/src/reducers/alerts.js
@@ -88,12 +88,20 @@ export default function(state = {}, action) {
case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS:
case CREATE_PROJECT_PHASE_SUCCESS: {
- Alert.success('Project phase created.')
+ if (state.project.version === 'v4') {
+ Alert.success('Project milestone created.')
+ } else {
+ Alert.success('Project phase created.')
+ }
return state
}
case DELETE_PROJECT_PHASE_SUCCESS: {
- Alert.success('Project phase deleted.')
+ if (state.project.version === 'v4') {
+ Alert.success('Project milestone deleted.')
+ } else {
+ Alert.success('Project phase deleted.')
+ }
return state
}
@@ -102,11 +110,11 @@ export default function(state = {}, action) {
Alert.success('Project deleted.')
return state
- case CREATE_TIMELINE_MILESTONE_SUCCESS:
+ case CREATE_TIMELINE_MILESTONE_SUCCESS:
Alert.success('Milestone created.')
return state
- case CREATE_TIMELINE_MILESTONE_FAILURE:
+ case CREATE_TIMELINE_MILESTONE_FAILURE:
Alert.error('Unable to create milestone')
return state
case COMPLETE_PRODUCT_MILESTONE_SUCCESS: