diff --git a/config/constants/development.js b/config/constants/development.js index 89dba60d..4619d647 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -31,5 +31,6 @@ module.exports = { DES_TRACK_ID: '5fa04185-041f-49a6-bfd1-fe82533cd6c8', DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', - SEGMENT_API_KEY: 'QBtLgV8vCiuRX1lDikbMjcoe9aCHkF6n' + SEGMENT_API_KEY: 'QBtLgV8vCiuRX1lDikbMjcoe9aCHkF6n', + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] } diff --git a/config/constants/production.js b/config/constants/production.js index f7f7e18b..835c1a23 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -31,5 +31,6 @@ module.exports = { DES_TRACK_ID: '5fa04185-041f-49a6-bfd1-fe82533cd6c8', DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', - SEGMENT_API_KEY: 'QSQAW5BWmZfLoKFNRgNKaqHvLDLJoGqF' + SEGMENT_API_KEY: 'QSQAW5BWmZfLoKFNRgNKaqHvLDLJoGqF', + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] } diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 13e8b58f..641b9542 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -28,9 +28,6 @@ import { UPLOAD_ATTACHMENT_FAILURE, UPLOAD_ATTACHMENT_PENDING, UPLOAD_ATTACHMENT_SUCCESS, - LOAD_CHALLENGE_RESOURCES_PENDING, - LOAD_CHALLENGE_RESOURCES_SUCCESS, - LOAD_CHALLENGE_RESOURCES_FAILURE, CREATE_CHALLENGE_RESOURCE, DELETE_CHALLENGE_RESOURCE, REMOVE_ATTACHMENT, @@ -40,7 +37,8 @@ import { UPDATE_CHALLENGE_DETAILS_FAILURE, CREATE_CHALLENGE_PENDING, CREATE_CHALLENGE_SUCCESS, - CREATE_CHALLENGE_FAILURE + CREATE_CHALLENGE_FAILURE, + LOAD_CHALLENGE_RESOURCES } from '../config/constants' import { loadProject } from './projects' @@ -174,7 +172,7 @@ export function loadChallengeDetails (projectId, challengeId) { payload: fetchChallenge(challengeId).then((challenge) => { // TODO remove this unncessary check, or better utilize the the case when given project id // does not match with challenge's project id - if (challenge.projectId === projectId) { + if (challenge.projectId == projectId) { // eslint-disable-line dispatch(loadProject(projectId)) } return challenge @@ -203,10 +201,12 @@ export function updateChallengeDetails (challengeId, challengeDetails) { type: UPDATE_CHALLENGE_DETAILS_SUCCESS, challengeDetails: challenge }) - }).catch(() => { + }).catch((error) => { dispatch({ - type: UPDATE_CHALLENGE_DETAILS_FAILURE + type: UPDATE_CHALLENGE_DETAILS_FAILURE, + error }) + return Promise.reject(error) }) } } @@ -394,27 +394,11 @@ export function loadChallengeTerms () { } export function loadResources (challengeId) { - return async (dispatch) => { - dispatch({ - type: LOAD_CHALLENGE_RESOURCES_PENDING, - challengeResources: {} - }) - + return (dispatch, getState) => { if (challengeId) { - return fetchResources(challengeId).then((resources) => { - dispatch({ - type: LOAD_CHALLENGE_RESOURCES_SUCCESS, - challengeResources: resources - }) - }).catch(() => { - dispatch({ - type: LOAD_CHALLENGE_RESOURCES_FAILURE - }) - }) - } else { - dispatch({ - type: LOAD_CHALLENGE_RESOURCES_SUCCESS, - challengeResources: null + return dispatch({ + type: LOAD_CHALLENGE_RESOURCES, + payload: fetchResources(challengeId) }) } } diff --git a/src/components/ChallengeEditor/ChallengeEditor.module.scss b/src/components/ChallengeEditor/ChallengeEditor.module.scss index 23c723f8..b66f644a 100644 --- a/src/components/ChallengeEditor/ChallengeEditor.module.scss +++ b/src/components/ChallengeEditor/ChallengeEditor.module.scss @@ -397,3 +397,9 @@ align-items: center; } +.errorContainer { + .errorMessage { + color: $red; + } +} + diff --git a/src/components/ChallengeEditor/ChallengePrizes-Field/index.js b/src/components/ChallengeEditor/ChallengePrizes-Field/index.js index b6b804c5..12a850d7 100644 --- a/src/components/ChallengeEditor/ChallengePrizes-Field/index.js +++ b/src/components/ChallengeEditor/ChallengePrizes-Field/index.js @@ -26,7 +26,7 @@ class ChallengePrizesField extends Component { addNewPrize () { const challengePrize = this.getChallengePrize() - challengePrize.prizes = [...challengePrize.prizes, { type: CHALLENGE_PRIZE_TYPE.MONEY, value: 1 }] + challengePrize.prizes = [...challengePrize.prizes, { type: CHALLENGE_PRIZE_TYPE.USD, value: 1 }] this.onUpdateValue(challengePrize) } @@ -55,7 +55,7 @@ class ChallengePrizesField extends Component { getChallengePrize () { const type = PRIZE_SETS_TYPE.CHALLENGE_PRIZES - return (this.props.challenge.prizeSets && this.props.challenge.prizeSets.length && this.props.challenge.prizeSets.find(p => p.type === type)) || { type, prizes: [{ type: CHALLENGE_PRIZE_TYPE.MONEY, value: 0 }] } + return (this.props.challenge.prizeSets && this.props.challenge.prizeSets.length && this.props.challenge.prizeSets.find(p => p.type === type)) || { type, prizes: [{ type: CHALLENGE_PRIZE_TYPE.USD, value: 0 }] } } renderPrizes () { diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 31b74caf..164787f6 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -21,7 +21,16 @@ import PhaseInput from '../../PhaseInput' import LegacyLinks from '../../LegacyLinks' import AssignedMemberField from '../AssignedMember-Field' -const ChallengeView = ({ projectDetail, challenge, metadata, challengeResources, token, isLoading, challengeId, assignedMemberDetails }) => { +const ChallengeView = ({ + projectDetail, + challenge, + metadata, + challengeResources, + token, + isLoading, + challengeId, + assignedMemberDetails, + enableEdit }) => { const selectedType = _.find(metadata.challengeTypes, { id: challenge.typeId }) const challengeTrack = _.find(metadata.challengeTracks, { id: challenge.trackId }) @@ -64,7 +73,7 @@ const ChallengeView = ({ projectDetail, challenge, metadata, challengeResources,
View Details
- + { enableEdit && }
@@ -202,7 +211,8 @@ ChallengeView.propTypes = { isLoading: PropTypes.bool.isRequired, challengeId: PropTypes.string.isRequired, challengeResources: PropTypes.arrayOf(PropTypes.object), - assignedMemberDetails: PropTypes.shape() + assignedMemberDetails: PropTypes.shape(), + enableEdit: PropTypes.bool } export default withRouter(ChallengeView) diff --git a/src/components/ChallengeEditor/CheckpointPrizes-Field/index.js b/src/components/ChallengeEditor/CheckpointPrizes-Field/index.js index 9afd7790..5f1fe512 100644 --- a/src/components/ChallengeEditor/CheckpointPrizes-Field/index.js +++ b/src/components/ChallengeEditor/CheckpointPrizes-Field/index.js @@ -4,17 +4,17 @@ import styles from './CheckpointPrizes-Field.module.scss' import cn from 'classnames' import { range } from 'lodash' import { validateValue } from '../../../util/input-check' -import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE } from '../../../config/constants' +import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE, CHALLENGE_PRIZE_TYPE } from '../../../config/constants' const CheckpointPrizesField = ({ challenge, onUpdateOthers }) => { const type = PRIZE_SETS_TYPE.CHECKPOINT_PRIZES - const checkpointPrize = challenge.prizeSets.find(p => p.type === type) || { type, prizes: [] } + const checkpointPrize = challenge.prizeSets.find(p => p.type === type) || { type: CHALLENGE_PRIZE_TYPE.USD, prizes: [] } const number = checkpointPrize.prizes.length const amount = checkpointPrize.prizes.length ? checkpointPrize.prizes[0].value : 0 function onChange (number, amount) { checkpointPrize.prizes = range(validateValue(number, VALIDATION_VALUE_TYPE.INTEGER)) - .map(i => ({ type: 'Prize ' + i, value: validateValue(amount, VALIDATION_VALUE_TYPE.INTEGER, '$') })) + .map(i => ({ type: CHALLENGE_PRIZE_TYPE.USD, value: validateValue(amount, VALIDATION_VALUE_TYPE.INTEGER, '$') })) onUpdateOthers({ field: 'prizeSets', value: [...challenge.prizeSets.filter(p => p.type !== type), +number && checkpointPrize].filter(p => p) }) } return ( diff --git a/src/components/ChallengeEditor/CopilotFee-Field/index.js b/src/components/ChallengeEditor/CopilotFee-Field/index.js index d7d357de..fbee9df3 100644 --- a/src/components/ChallengeEditor/CopilotFee-Field/index.js +++ b/src/components/ChallengeEditor/CopilotFee-Field/index.js @@ -5,11 +5,11 @@ import PropTypes from 'prop-types' import { validateValue } from '../../../util/input-check' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faDollarSign } from '@fortawesome/free-solid-svg-icons' -import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE } from '../../../config/constants' +import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE, CHALLENGE_PRIZE_TYPE } from '../../../config/constants' const CopilotFeeField = ({ challenge, onUpdateOthers, readOnly }) => { const type = PRIZE_SETS_TYPE.COPILOT_PAYMENT - const copilotFee = (challenge.prizeSets && challenge.prizeSets.find(p => p.type === type)) || { type, prizes: [{ type, value: 0 }] } + const copilotFee = (challenge.prizeSets && challenge.prizeSets.find(p => p.type === type)) || { type, prizes: [{ type: CHALLENGE_PRIZE_TYPE.USD, value: 0 }] } const value = copilotFee.prizes[0].value function onChange (e) { @@ -17,7 +17,7 @@ const CopilotFeeField = ({ challenge, onUpdateOthers, readOnly }) => { if (parseInt(value) > 1000000) { value = '1000000' } - copilotFee.prizes = [{ type, value }] + copilotFee.prizes = [{ type: CHALLENGE_PRIZE_TYPE.USD, value }] onUpdateOthers({ field: 'prizeSets', value: [...challenge.prizeSets.filter(p => p.type !== type), copilotFee] }) } diff --git a/src/components/ChallengeEditor/ReviewCost-Field/index.js b/src/components/ChallengeEditor/ReviewCost-Field/index.js index 11f288fc..03099bf6 100644 --- a/src/components/ChallengeEditor/ReviewCost-Field/index.js +++ b/src/components/ChallengeEditor/ReviewCost-Field/index.js @@ -3,16 +3,16 @@ import PropTypes from 'prop-types' import styles from './ReviewCost-Field.module.scss' import cn from 'classnames' import { validateValue } from '../../../util/input-check' -import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE } from '../../../config/constants' +import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE, CHALLENGE_PRIZE_TYPE } from '../../../config/constants' const ReviewCostField = ({ challenge, onUpdateOthers }) => { const type = PRIZE_SETS_TYPE.REVIEWER_PAYMENT - const reviewCost = challenge.prizeSets.find(p => p.type === type) || { type, prizes: [{ type, value: 0 }] } + const reviewCost = challenge.prizeSets.find(p => p.type === type) || { type, prizes: [{ type: CHALLENGE_PRIZE_TYPE.USD, value: 0 }] } const value = reviewCost.prizes[0].value function onChange (e) { const value = validateValue(e.target.value, VALIDATION_VALUE_TYPE.INTEGER, '$') - reviewCost.prizes = [{ type, value }] + reviewCost.prizes = [{ type: CHALLENGE_PRIZE_TYPE.USD, value }] onUpdateOthers({ field: 'prizeSets', value: [...challenge.prizeSets.filter(p => p.type !== type), reviewCost] }) } diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 08118594..8f6d4433 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -15,7 +15,8 @@ import { PRIZE_SETS_TYPE, DEFAULT_TERM_UUID, DEFAULT_NDA_UUID, - SUBMITTER_ROLE_UUID + SUBMITTER_ROLE_UUID, + CREATE_FORUM_TYPE_IDS } from '../../config/constants' import { PrimaryButton, OutlineButton } from '../Buttons' import TrackField from './Track-Field' @@ -767,6 +768,10 @@ class ChallengeEditor extends Component { phases: this.getTemplatePhases(defaultTemplate) // prizeSets: this.getDefaultPrizeSets() } + const discussions = this.getDiscussionsConfig(newChallenge) + if (discussions) { + newChallenge.discussions = discussions + } try { const action = await createChallenge(newChallenge) const draftChallenge = { @@ -779,6 +784,18 @@ class ChallengeEditor extends Component { } } + getDiscussionsConfig (challenge) { + if (_.includes(CREATE_FORUM_TYPE_IDS, challenge.typeId)) { + return ([ + { + name: `${challenge.name} Discussion`, + type: 'challenge', + provider: 'vanilla' + } + ]) + } + } + getTemplatePhases (template) { const timelinePhaseIds = template.phases.map(timelinePhase => timelinePhase.phaseId || timelinePhase) const validPhases = _.cloneDeep(this.props.metadata.challengePhases).filter(challengePhase => { @@ -919,11 +936,7 @@ class ChallengeEditor extends Component { } async onlySave () { - this.updateAllChallengeInfo(this.state.challenge.status, () => { - this.resetModal() - const { history } = this.props - history.push('./view') - }) + this.updateAllChallengeInfo(this.state.challenge.status) } getResourceRoleByName (name) { @@ -1008,7 +1021,7 @@ class ChallengeEditor extends Component { return
Error loading challenge
} const isTask = _.get(challenge, 'task.isTask', false) - const { assignedMemberDetails } = this.state + const { assignedMemberDetails, error } = this.state let isActive = false let isDraft = false let isCompleted = false @@ -1044,7 +1057,6 @@ class ChallengeEditor extends Component { let activateModal = null let closeTaskModal = null let draftModal = null - let savedModal = null let { type } = challenge if (!type) { @@ -1056,20 +1068,6 @@ class ChallengeEditor extends Component { } } } - if (!isNew && challenge.status === 'New' && isLaunch && isConfirm) { - savedModal = ( - - ) - } if (!isNew && isLaunch && !isConfirm) { activateModal = ( @@ -1162,6 +1160,8 @@ class ChallengeEditor extends Component { ) } + const errorContainer =
{error}
+ const actionButtons = {!isLoading && this.state.hasValidationErrors &&
Please fix the errors before saving
} { @@ -1182,7 +1182,7 @@ class ChallengeEditor extends Component {
{ isDraft &&
- +
} } {!isLoading && isActive &&
@@ -1211,10 +1211,11 @@ class ChallengeEditor extends Component {
+ { errorContainer } { actionButtons } ) : ( -
+ e.preventDefault()}>
@@ -1331,6 +1332,7 @@ class ChallengeEditor extends Component {
+ { errorContainer } { actionButtons } ) @@ -1349,7 +1351,6 @@ class ChallengeEditor extends Component {
{ activateModal } { draftModal } - { savedModal } { closeTaskModal }
{ challengeForm } diff --git a/src/components/Sidebar/index.js b/src/components/Sidebar/index.js index a9be7f96..9e28cfdd 100644 --- a/src/components/Sidebar/index.js +++ b/src/components/Sidebar/index.js @@ -20,11 +20,11 @@ const Sidebar = ({ Active challenges
- +
Give Application Feedback
- +

Have an urgent issue?
E: support@topcoder.com diff --git a/src/config/constants.js b/src/config/constants.js index e8b1072b..5ae0d333 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -16,6 +16,7 @@ export const { QA_TRACK_ID, SEGMENT_API_KEY } = process.env +export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS // Actions export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS' @@ -65,6 +66,7 @@ export const UPLOAD_ATTACHMENT_PENDING = 'UPLOAD_ATTACHMENT_PENDING' export const UPLOAD_ATTACHMENT_FAILURE = 'UPLOAD_ATTACHMENT_FAILURE' export const UPLOAD_ATTACHMENT_SUCCESS = 'UPLOAD_ATTACHMENT_SUCCESS' +export const LOAD_CHALLENGE_RESOURCES = 'LOAD_CHALLENGE_RESOURCES' export const LOAD_CHALLENGE_RESOURCES_SUCCESS = 'LOAD_CHALLENGE_RESOURCES_SUCCESS' export const LOAD_CHALLENGE_RESOURCES_PENDING = 'LOAD_CHALLENGE_RESOURCES_PENDING' export const LOAD_CHALLENGE_RESOURCES_FAILURE = 'LOAD_CHALLENGE_RESOURCES_FAILURE' @@ -134,7 +136,7 @@ export const VALIDATION_VALUE_TYPE = { } export const CHALLENGE_PRIZE_TYPE = { - MONEY: 'money' + USD: 'USD' } export const ALLOWED_USER_ROLES = [ diff --git a/src/containers/ChallengeEditor/ChallengeEditor.module.scss b/src/containers/ChallengeEditor/ChallengeEditor.module.scss new file mode 100644 index 00000000..ce43f8f3 --- /dev/null +++ b/src/containers/ChallengeEditor/ChallengeEditor.module.scss @@ -0,0 +1,7 @@ +@import "../../styles/includes"; + + +.errorContainer { + color: $red; + padding: 10px; +} \ No newline at end of file diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 7f6e35a0..75aae0c2 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -5,6 +5,7 @@ import { withRouter, Route } from 'react-router-dom' import ChallengeEditorComponent from '../../components/ChallengeEditor' import ChallengeViewComponent from '../../components/ChallengeEditor/ChallengeView' import Loader from '../../components/Loader' +import styles from './ChallengeEditor.module.scss' import { loadTimelineTemplates, @@ -113,10 +114,21 @@ class ChallengeEditor extends Component { loadChallengeDetails(projectId, challengeId) } + isEditable () { + const { hasProjectAccess, metadata: { resourceRoles }, challengeResources, loggedInUser } = this.props + if (!hasProjectAccess) { + return false + } + const userRoles = _.filter(challengeResources, cr => cr.memberId === `${loggedInUser.userId}`) + const userResourceRoles = _.filter(resourceRoles, rr => _.some(userRoles, ur => ur.roleId === rr.id)) + return _.some(userResourceRoles, urr => urr.fullAccess && urr.isActive) + } + render () { const { match, isLoading, + isProjectLoading, challengeDetails, challengeResources, metadata, @@ -132,6 +144,7 @@ class ChallengeEditor extends Component { replaceResourceInRole // members } = this.props + if (isProjectLoading || isLoading) return const challengeId = _.get(match.params, 'challengeId', null) if (challengeId && (!challengeDetails || !challengeDetails.id)) { return () @@ -144,6 +157,7 @@ class ChallengeEditor extends Component { handle: submitters[0].memberHandle } } + const enableEdit = this.isEditable() return

)) } /> - You don't have access to edit the challenge
} + { enableEdit && (( @@ -196,6 +211,7 @@ class ChallengeEditor extends Component { /> )) } /> + } )) } /> @@ -237,6 +254,8 @@ ChallengeEditor.propTypes = { loadResourceRoles: PropTypes.func, challengeResources: PropTypes.arrayOf(PropTypes.object), challengeDetails: PropTypes.object, + isProjectLoading: PropTypes.bool, + hasProjectAccess: PropTypes.bool, projectDetail: PropTypes.object, // history: PropTypes.object, metadata: PropTypes.shape({ @@ -246,6 +265,7 @@ ChallengeEditor.propTypes = { createAttachment: PropTypes.func, attachments: PropTypes.arrayOf(PropTypes.shape()), token: PropTypes.string, + loggedInUser: PropTypes.object, removeAttachment: PropTypes.func, failedToLoad: PropTypes.bool, loadMemberDetails: PropTypes.func, @@ -256,14 +276,17 @@ ChallengeEditor.propTypes = { // members: PropTypes.arrayOf(PropTypes.shape()) } -const mapStateToProps = ({ projects: { projectDetail }, challenges: { challengeDetails, challengeResources, metadata, isLoading, attachments, failedToLoad }, auth: { token }, members: { members } }) => ({ +const mapStateToProps = ({ projects, challenges: { challengeDetails, challengeResources, metadata, isLoading, attachments, failedToLoad }, auth: { token, user }, members: { members } }) => ({ challengeDetails, - projectDetail, + hasProjectAccess: projects.hasProjectAccess, + projectDetail: projects.projectDetail, challengeResources, metadata, isLoading, + isProjectLoading: projects.isLoading, attachments, token, + loggedInUser: user, failedToLoad // members }) diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 45d6a44c..df398975 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -87,7 +87,6 @@ export default function (state = initialState, action) { case LOAD_CHALLENGES_FAILURE: return { ...state, isLoading: false } case LOAD_CHALLENGE_DETAILS_FAILURE: - case UPDATE_CHALLENGE_DETAILS_FAILURE: case CREATE_CHALLENGE_FAILURE: return { ...state, isLoading: false, attachments: [], challenge: null, failedToLoad: true } case LOAD_CHALLENGE_DETAILS_SUCCESS: { @@ -143,6 +142,8 @@ export default function (state = initialState, action) { failedToLoad: false } } + case UPDATE_CHALLENGE_DETAILS_FAILURE: + return { ...state, isLoading: false, attachments: [], challenge: null, failedToLoad: false, failedToUpdate: true } case CREATE_CHALLENGE_SUCCESS: { // if we are showing the list of challenges with the same status as we just created, // then add the new challenge to the beginning of the current challenge list @@ -165,7 +166,7 @@ export default function (state = initialState, action) { case LOAD_CHALLENGE_RESOURCES_SUCCESS: return { ...state, - challengeResources: action.challengeResources, + challengeResources: action.payload, isLoading: false, failedToLoad: false } diff --git a/src/reducers/projects.js b/src/reducers/projects.js index 0d28f3a2..f80e9a93 100644 --- a/src/reducers/projects.js +++ b/src/reducers/projects.js @@ -1,6 +1,7 @@ /** * Reducer to process actions related to project */ +import _ from 'lodash' import { LOAD_PROJECT_DETAILS_FAILURE, LOAD_PROJECT_DETAILS_PENDING, @@ -16,12 +17,15 @@ export default function (state = initialState, action) { switch (action.type) { case LOAD_PROJECT_DETAILS_PENDING: return { ...state, isLoading: true } - case LOAD_PROJECT_DETAILS_FAILURE: - return { ...state, isLoading: false } + case LOAD_PROJECT_DETAILS_FAILURE: { + const status = _.get(action, 'payload.response.status', 500) + return { ...state, isLoading: false, hasProjectAccess: status !== 403 } + } case LOAD_PROJECT_DETAILS_SUCCESS: return { ...state, projectDetail: action.payload, + hasProjectAccess: true, isLoading: false } default: