diff --git a/.circleci/config.yml b/.circleci/config.yml index 76c515c40..2a42d6ba8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,7 +128,7 @@ workflows: - build-dev filters: branches: - only: ['dev', 'feature/cf-2.20'] + only: ['dev', 'feature/billing_account_protection'] - deployTest01: context : org-global diff --git a/src/api/billingAccounts.js b/src/api/billingAccounts.js index 5b986857e..639cbe944 100644 --- a/src/api/billingAccounts.js +++ b/src/api/billingAccounts.js @@ -12,3 +12,14 @@ export function fetchBillingAccounts(projectId) { return axios.get(`${TC_API_URL}/v5/projects/${projectId}/billingAccounts`) } + +/** + * Get billing account based on project id + * + * @param {String} projectId Id of the project + * + * @returns {Promise} Billing account data + */ +export function fetchBillingAccount(projectId) { + return axios.get(`${TC_API_URL}/v5/projects/${projectId}/billingAccount`) +} diff --git a/src/assets/icons/v.2.5/icon-warning.svg b/src/assets/icons/v.2.5/icon-warning.svg new file mode 100644 index 000000000..f3e59dfbe --- /dev/null +++ b/src/assets/icons/v.2.5/icon-warning.svg @@ -0,0 +1,13 @@ + + + + Created with Sketch. + + + + + + + + + diff --git a/src/config/constants.js b/src/config/constants.js index a1a0179c9..d88942685 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -120,6 +120,13 @@ export const CLEAR_PROJECT_SUGGESTIONS_SEARCH = 'CLEAR_PROJECT_SUGGESTIONS_SEA export const PROJECT_SUGGESTIONS_SEARCH_FAILURE = 'PROJECT_SUGGESTIONS_SEARCH_FAILURE' export const PROJECT_SUGGESTIONS_SEARCH_SUCCESS = 'PROJECT_SUGGESTIONS_SEARCH_SUCCESS' + +// project billingAccount +export const LOAD_PROJECT_BILLING_ACCOUNT = 'LOAD_PROJECT_BILLING_ACCOUNT' +export const LOAD_PROJECT_BILLING_ACCOUNT_PENDING = 'LOAD_PROJECT_BILLING_ACCOUNT_PENDING' +export const LOAD_PROJECT_BILLING_ACCOUNT_FAILURE = 'LOAD_PROJECT_BILLING_ACCOUNT_FAILURE' +export const LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS = 'LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS' + // Project Dashboard export const LOAD_PROJECT_DASHBOARD = 'LOAD_PROJECT_DASHBOARD' export const LOAD_PROJECT_DASHBOARD_PENDING = 'LOAD_PROJECT_DASHBOARD_PENDING' diff --git a/src/helpers/projectHelper.js b/src/helpers/projectHelper.js index bab78e28d..047a18a1d 100644 --- a/src/helpers/projectHelper.js +++ b/src/helpers/projectHelper.js @@ -20,6 +20,7 @@ import ReportsIcon from '../assets/icons/v.2.5/icon-reports.svg' import AssetsLibraryIcon from '../assets/icons/v.2.5/icon-assets-library.svg' import FAQIcon from '../assets/icons/faq.svg' import AccountSecurityIcon from 'assets/icons/v.2.5/icon-account-security.svg' +import WarningIcon from '../assets/icons/v.2.5/icon-warning.svg' import InvisibleIcon from '../assets/icons/invisible.svg' import { formatNumberWithCommas } from './format' @@ -259,8 +260,9 @@ export function getNewProjectLink(orgConfigs) { * Get the list of navigation links for project details view * @param {Object} project - The project object * @param {string} projectId - The project id + * @param {boolean} isBillingAccountExpired - is billingAccount expired */ -export function getProjectNavLinks(project, projectId, renderFAQs) { +export function getProjectNavLinks(project, projectId, renderFAQs, isBillingAccountExpired) { let messagesTab = null // `Discussions` items can be added as soon as project is loaded // if discussions are not hidden for it @@ -289,7 +291,11 @@ export function getProjectNavLinks(project, projectId, renderFAQs) { } if (hasPermission(PERMISSIONS.VIEW_PROJECT_SETTINGS)) { - navLinks.push({ label: 'Project Settings', to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + if (!isBillingAccountExpired) { + navLinks.push({ label: 'Project Settings', to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + }else { + navLinks.push({ label: Project Settings , to: `/projects/${projectId}/settings`, Icon: AccountSecurityIcon, iconClassName: 'stroke' }) + } } return navLinks diff --git a/src/projects/actions/project.js b/src/projects/actions/project.js index a4b64b3ad..848e2540d 100644 --- a/src/projects/actions/project.js +++ b/src/projects/actions/project.js @@ -13,6 +13,7 @@ import { getProjectById, createScopeChangeRequest as createScopeChangeRequestAPI, updateScopeChangeRequest as updateScopeChangeRequestAPI, } from '../../api/projects' +import {fetchBillingAccount} from '../../api/billingAccounts' import { getProjectInviteById, getProjectMemberInvites, @@ -26,6 +27,7 @@ import { } from '../../api/projectMembers' // import { loadProductTimelineWithMilestones } from './productsTimelines' import { + LOAD_PROJECT_BILLING_ACCOUNT, LOAD_PROJECT, LOAD_PROJECT_MEMBER_INVITE, CREATE_PROJECT, @@ -661,6 +663,15 @@ export function loadProjectMembers(projectId) { } } +export function loadProjectBillingAccount(projectId) { + return (dispatch) => { + return dispatch({ + type: LOAD_PROJECT_BILLING_ACCOUNT, + payload: fetchBillingAccount(projectId) + }) + } +} + export function loadProjectMemberInvites(projectId) { return (dispatch) => { return dispatch({ diff --git a/src/projects/actions/projectDashboard.js b/src/projects/actions/projectDashboard.js index efb9127e2..ef083cea9 100644 --- a/src/projects/actions/projectDashboard.js +++ b/src/projects/actions/projectDashboard.js @@ -1,5 +1,5 @@ import _ from 'lodash' -import { loadProject, loadProjectInvite, loadProjectMembers, loadProjectMemberInvites } from './project' +import { loadProject, loadProjectBillingAccount, loadProjectInvite, loadProjectMembers, loadProjectMemberInvites } from './project' import { loadProjectPlan } from './projectPlan' import { loadProjectsMetadata } from '../../actions/templates' import { LOAD_PROJECT_DASHBOARD, LOAD_ADDITIONAL_PROJECT_DATA } from '../../config/constants' @@ -27,10 +27,13 @@ const getDashboardData = (dispatch, getState, projectId, isOnlyLoadProjectInfo) //dispatch(loadMembers(userIds)), dispatch(loadProjectMembers(projectId)), dispatch(loadProjectMemberInvites(projectId)) + ] if (isOnlyLoadProjectInfo) { promises = [] } + // load project billingAccounts + promises.push(dispatch(loadProjectBillingAccount(projectId))) // for new projects load phases, products, project template and product templates if (['v3', 'v4'].indexOf(project.version) !== -1) { diff --git a/src/projects/detail/components/BillingAccountField/index.js b/src/projects/detail/components/BillingAccountField/index.js index f466e0e3c..d28a4bb1b 100644 --- a/src/projects/detail/components/BillingAccountField/index.js +++ b/src/projects/detail/components/BillingAccountField/index.js @@ -1,5 +1,6 @@ import React from 'react' import moment from 'moment' +import _ from 'lodash' import {HOC as hoc} from 'formsy-react' import Select from '../../../../components/Select/Select' @@ -27,6 +28,8 @@ class BillingAccountField extends React.Component { isLoading: true, billingAccounts: [], selectedBillingAccount: null, + defaultBillingAccount: null, + isDefaultBillingAccountExpired: false, } this.handleChange = this.handleChange.bind(this) @@ -60,6 +63,10 @@ class BillingAccountField extends React.Component { }) billingAccounts = [selectedBillingAccount, ...billingAccounts] + this.setState({ + defaultBillingAccount: selectedBillingAccount, + isDefaultBillingAccountExpired: this.props.isExpired + }) } } @@ -74,9 +81,16 @@ class BillingAccountField extends React.Component { handleChange(value) { this.setState({ selectedBillingAccount: value }) this.props.setValue(value ? value.value : null) + this.props.setBillingAccountExpired && this.props.setBillingAccountExpired(this.isCurrentBillingAccountExpired(value)) + } + + isCurrentBillingAccountExpired(nextSelectedBillingAccount) { + const {defaultBillingAccount, isDefaultBillingAccountExpired, selectedBillingAccount} = this.state + return isDefaultBillingAccountExpired && _.get(defaultBillingAccount, 'value') === _.get(nextSelectedBillingAccount || selectedBillingAccount, 'value') } render() { + const placeholder = this.state.billingAccounts.length > 0 ? 'Select billing account' : 'No Billing Account Available' @@ -92,6 +106,7 @@ class BillingAccountField extends React.Component { isDisabled={this.state.billingAccounts.length === 0} showDropdownIndicator /> + {this.isCurrentBillingAccountExpired() && Expired} {this.state.selectedBillingAccount && (
{ @@ -52,7 +59,7 @@ class EditProjectDefaultsForm extends React.Component { } handleSubmit() { - const {updateProject} = this.props + const {updateProject, loadProjectBillingAccount} = this.props const {id} = this.props.project const updateProjectObj = Object.keys(this.state.project) .filter(key => !_.isEqual(this.props.project[key], this.state.project[key])) @@ -61,7 +68,7 @@ class EditProjectDefaultsForm extends React.Component { return acc }, {}) updateProject(id, updateProjectObj) - .then(() => this.setState({enableButton: false})) + .then(() => this.setState({enableButton: false})).then(() => loadProjectBillingAccount(id)) .catch(console.error) } @@ -86,14 +93,16 @@ class EditProjectDefaultsForm extends React.Component {
@@ -105,7 +114,8 @@ class EditProjectDefaultsForm extends React.Component { } const mapDispatchToProps = { - updateProject + updateProject, + loadProjectBillingAccount } export default protectComponent( diff --git a/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx b/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx index 7a609a8a0..c5e59db9e 100644 --- a/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx +++ b/src/projects/detail/components/timeline/CreateMilestoneForm/CreateMilestoneForm.jsx @@ -34,13 +34,13 @@ class CreateMilestoneForm extends React.Component { cancelEdit() { const { previousMilestone } = this.props const startDate = previousMilestone ? moment.utc(previousMilestone.completionDate || previousMilestone.endDate) : moment.utc() - this.setState({ + this.state = { isEditing: false, type: '', name: '', startDate: startDate.format('YYYY-MM-DD'), endDate: startDate.add(3, 'days').format('YYYY-MM-DD') - }) + } } onAddClick() { diff --git a/src/projects/detail/containers/ProjectDefaultsContainer.jsx b/src/projects/detail/containers/ProjectDefaultsContainer.jsx index 450a22198..a23599514 100644 --- a/src/projects/detail/containers/ProjectDefaultsContainer.jsx +++ b/src/projects/detail/containers/ProjectDefaultsContainer.jsx @@ -18,6 +18,7 @@ class ProjectDefaultsContainer extends Component { isProcessing, feeds, isFeedsLoading, + isBillingAccountExpired, productsTimelines, phasesTopics, location, @@ -54,7 +55,7 @@ class ProjectDefaultsContainer extends Component { - + ) @@ -68,6 +69,7 @@ const mapStateToProps = ({ loadUser, projectState, projectTopics, topics }) => { return { user: loadUser.user, + isBillingAccountExpired: projectState.isBillingAccountExpired, isProcessing: projectState.processing, phases: projectState.phases, phasesNonDirty: projectState.phasesNonDirty, diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js index 1b81baefb..b9f89199a 100644 --- a/src/projects/detail/containers/ProjectInfoContainer.js +++ b/src/projects/detail/containers/ProjectInfoContainer.js @@ -83,6 +83,7 @@ class ProjectInfoContainer extends React.Component { !_.isEqual(nextProps.feeds, this.props.feeds) || !_.isEqual(nextProps.phases, this.props.phases) || !_.isEqual(nextProps.productsTimelines, this.props.productsTimelines) || + !_.isEqual(nextProps.isBillingAccountExpired, this.props.isBillingAccountExpired) || !_.isEqual(nextProps.phasesTopics, this.props.phasesTopics) || !_.isEqual(nextProps.isFeedsLoading, this.props.isFeedsLoading) || !_.isEqual(nextProps.isProjectProcessing, this.props.isProjectProcessing) || @@ -426,7 +427,7 @@ class ProjectInfoContainer extends React.Component { render() { const { showDeleteConfirm } = this.state const { project, phases, hideInfo, hideMembers, - productsTimelines, isProjectProcessing, notifications, projectTemplates } = this.props + productsTimelines, isProjectProcessing, notifications, projectTemplates, isBillingAccountExpired } = this.props const projectTemplateId = project.templateId const projectTemplateKey = _.get(project, 'details.products[0]') @@ -457,7 +458,7 @@ class ProjectInfoContainer extends React.Component { const notReadAssetsNotifications = filterFileAndLinkChangedNotifications(projectNotReadNotifications) const renderFAQs = containsFAQ(projectTemplate) - const navLinks = getProjectNavLinks(project, project.id, renderFAQs).map((navLink) => { + const navLinks = getProjectNavLinks(project, project.id, renderFAQs, isBillingAccountExpired).map((navLink) => { if (navLink.label === 'Messages') { navLink.count = notReadMessageNotifications.length navLink.toolTipText = 'New messages' @@ -593,10 +594,11 @@ ProjectInfoContainer.PropTypes = { canAccessPrivatePosts: PropTypes.bool.isRequired, } -const mapStateToProps = ({ templates, notifications }) => { +const mapStateToProps = ({ templates, projectState, notifications }) => { const canAccessPrivatePosts = hasPermission(PERMISSIONS.ACCESS_PRIVATE_POST) return ({ projectTemplates : templates.projectTemplates, + isBillingAccountExpired: projectState.isBillingAccountExpired, canAccessPrivatePosts, notifications: notifications.notifications, }) diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index 5ceb3a85d..a877ce308 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -1,6 +1,7 @@ import { CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_PENDING, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_FAILURE, LOAD_PROJECT_PENDING, LOAD_PROJECT_SUCCESS, LOAD_PROJECT_MEMBER_INVITE_PENDING, LOAD_PROJECT_MEMBER_INVITE_SUCCESS, LOAD_PROJECT_FAILURE, + LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS, LOAD_PROJECT_BILLING_ACCOUNT_FAILURE, CREATE_PROJECT_PENDING, CREATE_PROJECT_SUCCESS, CREATE_PROJECT_FAILURE, CLEAR_LOADED_PROJECT, UPDATE_PROJECT_PENDING, UPDATE_PROJECT_SUCCESS, UPDATE_PROJECT_FAILURE, DELETE_PROJECT_PENDING, DELETE_PROJECT_SUCCESS, DELETE_PROJECT_FAILURE, @@ -36,6 +37,7 @@ export function getEmptyProjectObject() { const initialState = { isLoading: true, + isBillingAccountExpired: false, processing: false, processingMembers: false, updatingMemberIds: [], @@ -151,6 +153,16 @@ export const projectState = function (state=initialState, action) { return Object.assign({}, state, { isCreatingPhase: true }) + case LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS: + return { + ...state, + isBillingAccountExpired: !action.payload.data.active, + } + case LOAD_PROJECT_BILLING_ACCOUNT_FAILURE: + return { + ...state, + isBillingAccountExpired: false, + } case CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS: case CREATE_PROJECT_PHASE_SUCCESS: { const { phase, product } = action.payload diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index 9ed828d00..dbbca6a6f 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -2,6 +2,8 @@ import _ from 'lodash' import Alert from 'react-s-alert' /* eslint-disable no-unused-vars */ import { + // billing account + LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS, // bulk phase and milestones CREATE_PROJECT_PHASE_TIMELINE_MILESTONES_SUCCESS, // Project @@ -168,6 +170,11 @@ export default function(state = {}, action) { case CANCEL_SCOPE_CHANGE_FAILURE: Alert.error('Unable to Cancel the Scope Change') return state + case LOAD_PROJECT_BILLING_ACCOUNT_SUCCESS: + if (!action.payload.data.active) { + Alert.error('The billing account of this project is expired, please update it.') + } + return state case ACTIVATE_SCOPE_CHANGE_SUCCESS: Alert.success('Activated the Scope Change successfully')