diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 6661b4c5..13e8b58f 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -20,10 +20,7 @@ import { deleteResource as deleteResourceAPI } from '../services/challenges' import { - LOAD_CHALLENGE_DETAILS_PENDING, - LOAD_CHALLENGE_DETAILS_SUCCESS, - LOAD_CHALLENGE_DETAILS_FAILURE, - LOAD_CHALLENGE_MEMBERS_SUCCESS, + LOAD_CHALLENGE_DETAILS, LOAD_CHALLENGE_METADATA_SUCCESS, LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, @@ -45,7 +42,6 @@ import { CREATE_CHALLENGE_SUCCESS, CREATE_CHALLENGE_FAILURE } from '../config/constants' -import { fetchProjectById } from '../services/projects' import { loadProject } from './projects' /** @@ -171,41 +167,19 @@ export function loadChallenges (projectId, status, filterChallengeName = null) { * Loads Challenge details */ export function loadChallengeDetails (projectId, challengeId) { - return async (dispatch, getState) => { - dispatch({ - type: LOAD_CHALLENGE_DETAILS_PENDING, - challengeDetails: {} - }) - + return (dispatch, getState) => { if (challengeId) { - fetchChallenge(challengeId).then((challenge) => { - dispatch({ - type: LOAD_CHALLENGE_DETAILS_SUCCESS, - challengeDetails: challenge - }) - loadProject(challenge.projectId)(dispatch, getState) - }).catch(() => { - dispatch({ - type: LOAD_CHALLENGE_DETAILS_FAILURE + return dispatch({ + type: LOAD_CHALLENGE_DETAILS, + 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) { + dispatch(loadProject(projectId)) + } + return challenge }) }) - } else { - dispatch({ - type: LOAD_CHALLENGE_DETAILS_SUCCESS, - challengeDetails: null - }) - - if (projectId) { - fetchProjectById(projectId).then((selectedProject) => { - if (!selectedProject) return - const members = selectedProject.members - .filter(m => m.role === 'manager' || m.role === 'copilot') - dispatch({ - type: LOAD_CHALLENGE_MEMBERS_SUCCESS, - members - }) - }) - } } } } @@ -427,7 +401,7 @@ export function loadResources (challengeId) { }) if (challengeId) { - fetchResources(challengeId).then((resources) => { + return fetchResources(challengeId).then((resources) => { dispatch({ type: LOAD_CHALLENGE_RESOURCES_SUCCESS, challengeResources: resources @@ -457,6 +431,12 @@ export function loadResourceRoles () { } } +/** + * Deletes a resource for the given challenge in given role + * @param {UUID} challengeId id of the challenge for which resource is to be deleted + * @param {UUID} roleId id of the role, the resource is in + * @param {String} memberHandle handle of the resource + */ export function deleteResource (challengeId, roleId, memberHandle) { const resource = { challengeId, @@ -471,6 +451,12 @@ export function deleteResource (challengeId, roleId, memberHandle) { } } +/** + * Creates a resource for the given challenge in given role + * @param {UUID} challengeId id of the challenge for which resource is to be created + * @param {UUID} roleId id of the role, the resource should be in + * @param {String} memberHandle handle of the resource + */ export function createResource (challengeId, roleId, memberHandle) { const resource = { challengeId, @@ -485,6 +471,13 @@ export function createResource (challengeId, roleId, memberHandle) { } } +/** + * Replaces the given resource in given role with new resource for the provided challenge + * @param {UUID} challengeId id of the challenge for which resource is to be replaced + * @param {UUID} roleId id of the role, the resource is in + * @param {String} newMember handle of the new resource + * @param {String} oldMember handle of the existing resource + */ export function replaceResourceInRole (challengeId, roleId, newMember, oldMember) { return async (dispatch) => { if (newMember === oldMember) { diff --git a/src/actions/projects.js b/src/actions/projects.js index 8c86a4f2..cd93004e 100644 --- a/src/actions/projects.js +++ b/src/actions/projects.js @@ -1,8 +1,6 @@ import { - LOAD_PROJECT_DETAILS_SUCCESS, - LOAD_PROJECT_DETAILS_PENDING, - LOAD_PROJECT_DETAILS_FAILURE, - LOAD_CHALLENGE_MEMBERS_SUCCESS + LOAD_CHALLENGE_MEMBERS_SUCCESS, + LOAD_PROJECT_DETAILS } from '../config/constants' import { fetchProjectById } from '../services/projects' @@ -10,18 +8,10 @@ import { fetchProjectById } from '../services/projects' * Loads project details */ export function loadProject (projectId) { - return async (dispatch, getState) => { - dispatch({ - type: LOAD_PROJECT_DETAILS_PENDING, - projectDetail: {} - }) - if (projectId) { - fetchProjectById(projectId).then((project) => { - dispatch({ - type: LOAD_PROJECT_DETAILS_SUCCESS, - projectDetail: project - }) - + return (dispatch, getState) => { + return dispatch({ + type: LOAD_PROJECT_DETAILS, + payload: fetchProjectById(projectId).then((project) => { if (project && project.members) { const members = project.members.filter(m => m.role === 'manager' || m.role === 'copilot') dispatch({ @@ -29,16 +19,8 @@ export function loadProject (projectId) { members }) } - }).catch(() => { - dispatch({ - type: LOAD_PROJECT_DETAILS_FAILURE - }) + return project }) - } else { - dispatch({ - type: LOAD_PROJECT_DETAILS_SUCCESS, - projectDetail: null - }) - } + }) } } diff --git a/src/components/ChallengeEditor/FinalDeliverables-Field/index.js b/src/components/ChallengeEditor/FinalDeliverables-Field/index.js index 57ead390..fbec690e 100644 --- a/src/components/ChallengeEditor/FinalDeliverables-Field/index.js +++ b/src/components/ChallengeEditor/FinalDeliverables-Field/index.js @@ -34,7 +34,7 @@ class FinalDeliverablesField extends Component { const { challenge, readOnly, removeFileType } = this.props const fileTypesMetadata = _.find(challenge.metadata, { name: 'fileTypes' }) const fileTypes = (fileTypesMetadata && JSON.parse(fileTypesMetadata.value)) || [] - const isDuplicateValue = _.includes(fileTypes, this.state.newFileType.trim()) + const isDuplicateValue = _.includes(fileTypes.map((fileType) => fileType.toLowerCase()), this.state.newFileType.toLowerCase().trim()) return ( diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 682c4274..08118594 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -113,30 +113,18 @@ class ChallengeEditor extends Component { this.getTemplatePhases = this.getTemplatePhases.bind(this) this.getAvailableTimelineTemplates = this.getAvailableTimelineTemplates.bind(this) this.autoUpdateChallengeThrottled = _.throttle(this.autoUpdateChallenge.bind(this), 3000) // 3s - this.resetChallengeData((newState, finish) => { - this.state = { - ...this.state, - ...newState - } - if (finish) { - finish() - } - }) } - componentDidUpdate () { + componentDidMount () { this.resetChallengeData(this.setState.bind(this)) } - componentWillReceiveProps (nextProps) { - // if member details weren't initially loaded and now they got loaded, then set them to the state - if (!this.state.assignedMemberDetails && nextProps.assignedMemberDetails) { - this.setState({ assignedMemberDetails: nextProps.assignedMemberDetails }) - } + componentDidUpdate () { + this.resetChallengeData(this.setState.bind(this)) } async resetChallengeData (setState = () => {}) { - const { isNew, challengeDetails, metadata, attachments, challengeId } = this.props + const { isNew, challengeDetails, metadata, attachments, challengeId, assignedMemberDetails } = this.props if ( challengeDetails && challengeDetails.id && @@ -160,10 +148,11 @@ class ChallengeEditor extends Component { } challengeData.copilot = copilot || copilotFromResources challengeData.reviewer = reviewer || reviewerFromResources - const challengeDetail = { ...dropdowns['newChallenge'], ...challengeData } + const challengeDetail = { ...challengeData } const isOpenAdvanceSettings = challengeDetail.groups.length > 0 setState({ challenge: challengeDetail, + assignedMemberDetails, draftChallenge: { data: { ..._.cloneDeep(challengeDetails), copilot: challengeData.copilot, @@ -303,7 +292,7 @@ class ChallengeEditor extends Component { */ onUpdateAssignedMember (option) { const { challenge: oldChallenge } = this.state - const newChallenge = { ...oldChallenge, task: { isAssigned: false, memberId: null, isTask: true } } + const newChallenge = { ...oldChallenge } let assignedMemberDetails if (option && option.value) { @@ -716,7 +705,6 @@ class ChallengeEditor extends Component { 'startDate', 'terms', 'prizeSets', - 'task', 'winners' ], this.state.challenge) challenge.legacy = _.assign(this.state.challenge.legacy, { diff --git a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss index bf8e9417..5613eaa9 100644 --- a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss +++ b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss @@ -85,6 +85,7 @@ flex: 6; flex-wrap: nowrap; display: flex; + text-decoration: none; .name { flex:1; @@ -102,6 +103,7 @@ flex-direction: column; justify-content: center; align-items: center; + text-decoration: none; } .col3 { @@ -180,6 +182,26 @@ } } } + + .createdAt { + color: $gray; + font-size: 10px; + + &:hover { + text-decoration: none; + } + } + + .lastUpdated { + text-decoration: none; + .lastUpdatedAt { + color: $tc-black; + } + .lastUpdatedBy { + color: $gray; + font-size: 10px; + } + } } .activateButton { diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index e42371c2..c832e5af 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -13,7 +13,7 @@ import { faFile, faUser } from '@fortawesome/free-solid-svg-icons' import ChallengeStatus from '../ChallengeStatus' import ChallengeTag from '../ChallengeTag' import styles from './ChallengeCard.module.scss' -import { getFormattedDuration } from '../../../util/date' +import { getFormattedDuration, formatDate } from '../../../util/date' import { CHALLENGE_STATUS, COMMUNITY_APP_URL, DIRECT_PROJECT_URL, ONLINE_REVIEW_URL } from '../../../config/constants' import { patchChallenge } from '../../../services/challenges' import ConfirmationModal from '../../Modal/ConfirmationModal' @@ -172,6 +172,15 @@ const renderStatus = (status) => { } } +const renderLastUpdated = (challenge) => { + return ( + +
{formatDate(challenge.updated)}
+
{challenge.updatedBy}
+ + ) +} + class ChallengeCard extends React.Component { constructor (props) { super(props) @@ -206,7 +215,7 @@ class ChallengeCard extends React.Component { try { this.setState({ isSaving: true }) const response = await patchChallenge(challenge.id, { status: 'Active' }) - this.setState({ isLaunch: true, isConfirm: response.data.id, isSaving: false }) + this.setState({ isLaunch: true, isConfirm: response.id, isSaving: false }) } catch (e) { const error = _.get(e, 'response.data.message', 'Unable to activate the challenge') this.setState({ isSaving: false, error }) @@ -248,8 +257,10 @@ class ChallengeCard extends React.Component {
{challenge.name} + {`Created by ${challenge.createdBy} at ${formatDate(challenge.created)}`}
+ {renderLastUpdated(challenge)} {renderStatus(challenge.status.toUpperCase())} diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index b1c2e30f..552050c0 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -192,6 +192,7 @@ class ChallengeList extends Component { challenges.length > 0 && (
Challenges Name
+
Last Updated
Status
{(selectedTab === 0) && (
Current phase
)}
 
diff --git a/src/components/Track/index.js b/src/components/Track/index.js index a1d5c262..999106e2 100644 --- a/src/components/Track/index.js +++ b/src/components/Track/index.js @@ -7,14 +7,14 @@ import styles from './Track.module.scss' const assets = require.context('../../assets/images/tracks', false, /svg/) const Track = ({ type, isActive, onUpdateOthers, disabled }) => { - const icon = `./${type.abbreviation.toLowerCase()}.svg` + const icon = type ? `./${type.abbreviation.toLowerCase()}.svg` : '' return (
onUpdateOthers({ field: 'trackId', value: type.id })}>
{ assets && assets.keys().includes(icon) ? : '' }
- {type.name} + {type ? type.name : 'Unkown'}
) } diff --git a/src/config/constants.js b/src/config/constants.js index 5f4ad9a0..e8b1072b 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -34,6 +34,7 @@ export const LOAD_CHALLENGES_SUCCESS = 'LOAD_CHALLENGES_SUCCESS' export const LOAD_CHALLENGES_PENDING = 'LOAD_CHALLENGES_PENDING' export const LOAD_CHALLENGES_FAILURE = 'LOAD_CHALLENGES_FAILURE' +export const LOAD_CHALLENGE_DETAILS = 'LOAD_CHALLENGE_DETAILS' export const LOAD_CHALLENGE_DETAILS_SUCCESS = 'LOAD_CHALLENGE_DETAILS_SUCCESS' export const LOAD_CHALLENGE_DETAILS_PENDING = 'LOAD_CHALLENGE_DETAILS_PENDING' export const LOAD_CHALLENGE_DETAILS_FAILURE = 'LOAD_CHALLENGE_DETAILS_FAILURE' @@ -46,6 +47,7 @@ export const CREATE_CHALLENGE_SUCCESS = 'CREATE_CHALLENGE_SUCCESS' export const CREATE_CHALLENGE_PENDING = 'CREATE_CHALLENGE_PENDING' export const CREATE_CHALLENGE_FAILURE = 'CREATE_CHALLENGE_FAILURE' +export const LOAD_PROJECT_DETAILS = 'LOAD_PROJECT_DETAILS' export const LOAD_PROJECT_DETAILS_SUCCESS = 'LOAD_PROJECT_DETAILS_SUCCESS' export const LOAD_PROJECT_DETAILS_PENDING = 'LOAD_PROJECT_DETAILS_PENDING' export const LOAD_PROJECT_DETAILS_FAILURE = 'LOAD_PROJECT_DETAILS_FAILURE' @@ -54,7 +56,7 @@ export const LOAD_CHALLENGE_SUBMISSIONS_SUCCESS = 'LOAD_CHALLENGE_SUBMISSIONS_SU export const LOAD_CHALLENGE_SUBMISSIONS_PENDING = 'LOAD_CHALLENGE_SUBMISSIONS_PENDING' export const LOAD_CHALLENGE_SUBMISSIONS_FAILURE = 'LOAD_CHALLENGE_SUBMISSIONS_FAILURE' -export const LOAD_CHALLENGE_MEMBERS_SUCCESS = 'LOAD_CHALLENGE_MEMBERS' +export const LOAD_CHALLENGE_MEMBERS_SUCCESS = 'LOAD_CHALLENGE_MEMBERS_SUCCESS' export const LOAD_CHALLENGE_METADATA_SUCCESS = 'LOAD_CHALLENGE_METADATA_SUCCESS' export const SAVE_AUTH_TOKEN = 'SAVE_AUTH_TOKEN' diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index ef853d94..7f6e35a0 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -105,10 +105,11 @@ class ChallengeEditor extends Component { } } - fetchChallengeDetails (newMatch, loadChallengeDetails, loadResources) { - const projectId = _.get(newMatch.params, 'projectId', null) + async fetchChallengeDetails (newMatch, loadChallengeDetails, loadResources) { + let projectId = _.get(newMatch.params, 'projectId', null) + projectId = projectId ? parseInt(projectId) : null const challengeId = _.get(newMatch.params, 'challengeId', null) - loadResources(challengeId) + await loadResources(challengeId) loadChallengeDetails(projectId, challengeId) } diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index f2ec3292..c24257bc 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -35,9 +35,9 @@ class Challenges extends Component { if (menu === 'NULL' && activeProjectId !== -1) { resetSidebarActiveParams() } else { - // this.props.loadChallengesByPage(1, projectId ? parseInt(projectId) : -1, CHALLENGE_STATUS.ACTIVE, '') if (projectId) { this.props.loadProject(projectId) + this.reloadChallenges(this.props) } } } diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index f9237977..45d6a44c 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -90,14 +90,15 @@ export default function (state = initialState, action) { case UPDATE_CHALLENGE_DETAILS_FAILURE: case CREATE_CHALLENGE_FAILURE: return { ...state, isLoading: false, attachments: [], challenge: null, failedToLoad: true } - case LOAD_CHALLENGE_DETAILS_SUCCESS: + case LOAD_CHALLENGE_DETAILS_SUCCESS: { return { ...state, - challengeDetails: action.challengeDetails, + challengeDetails: action.payload, isLoading: false, - attachments: _.has(action.challengeDetails, 'attachments') ? action.challengeDetails.attachments : [], + attachments: _.has(action.payload, 'attachments') ? action.payload.attachments : [], failedToLoad: false } + } case UPDATE_CHALLENGE_DETAILS_SUCCESS: { // During editing the challenge we might change its status, so when we came back to the challenge list // updated challenge might have to be removed from the list, or added to the list, or just updated @@ -213,8 +214,9 @@ export default function (state = initialState, action) { [action.metadataKey]: action.metadataValue } } - case LOAD_CHALLENGE_MEMBERS_SUCCESS: + case LOAD_CHALLENGE_MEMBERS_SUCCESS: { return { ...state, metadata: { ...state.metadata, members: action.members } } + } case UPLOAD_ATTACHMENT_PENDING: return { ...state, isUploading: true, isSuccess: false, uploadingId: action.challengeId } case UPLOAD_ATTACHMENT_SUCCESS: diff --git a/src/reducers/projects.js b/src/reducers/projects.js index 9d17ba7f..0d28f3a2 100644 --- a/src/reducers/projects.js +++ b/src/reducers/projects.js @@ -21,7 +21,7 @@ export default function (state = initialState, action) { case LOAD_PROJECT_DETAILS_SUCCESS: return { ...state, - projectDetail: action.projectDetail, + projectDetail: action.payload, isLoading: false } default: diff --git a/src/routes.js b/src/routes.js index 6e83c648..f0c20d00 100644 --- a/src/routes.js +++ b/src/routes.js @@ -13,11 +13,50 @@ import ChallengeList from './containers/Challenges' import ChallengeEditor from './containers/ChallengeEditor' import { getFreshToken } from 'tc-accounts' import { saveToken } from './actions/auth' +import { loadChallengeDetails } from './actions/challenges' import { connect } from 'react-redux' import { checkAllowedRoles } from './util/tc' const { ACCOUNTS_APP_LOGIN_URL } = process.env +class RedirectToChallenge extends React.Component { + componentWillMount () { + const { match, loadChallengeDetails } = this.props + const challengeId = match.params.challengeId + loadChallengeDetails(null, challengeId) + } + + componentWillReceiveProps (nextProps) { + const projectId = _.get(nextProps.challengeDetails, 'projectId') + const challengeId = _.get(nextProps.challengeDetails, 'id') + if (projectId && challengeId) { + console.log('Redircting to full URL') + this.props.history.replace(`/projects/${projectId}/challenges/${challengeId}/view`) + } + } + + render () { + return
Redirecting...
+ } +} + +let mapStateToProps = ({ challenges: { challengeDetails } }) => ({ + challengeDetails +}) + +let mapDispatchToProps = { + loadChallengeDetails +} + +RedirectToChallenge.propTypes = { + loadChallengeDetails: PropTypes.func, + challengeDetails: PropTypes.object, + match: PropTypes.object, + history: PropTypes.object +} + +const ConnectRedirectToChallenge = connect(mapStateToProps, mapDispatchToProps)(RedirectToChallenge) + class Routes extends React.Component { componentWillMount () { this.checkAuth() @@ -72,6 +111,7 @@ class Routes extends React.Component { , )()} /> + renderApp( @@ -92,11 +132,11 @@ class Routes extends React.Component { } } -const mapStateToProps = ({ auth }) => ({ +mapStateToProps = ({ auth }) => ({ ...auth }) -const mapDispatchToProps = { +mapDispatchToProps = { saveToken } diff --git a/src/util/date.js b/src/util/date.js index ac13c371..b5017f29 100644 --- a/src/util/date.js +++ b/src/util/date.js @@ -17,6 +17,15 @@ export const getLastDate = (dates) => { return Math.max(...dates) } +/** + * Get formatted date + * @param {Date} date date to be formatted + * @returns formatted Date + */ +export const formatDate = (date) => { + return moment(date).format('DD/MM/YYYY') +} + /** * Formats duration * @param duration Duration