diff --git a/src/shared/actions/page/challenge-details.js b/src/shared/actions/page/challenge-details.js index 4130d79e6c..a06218e8c8 100644 --- a/src/shared/actions/page/challenge-details.js +++ b/src/shared/actions/page/challenge-details.js @@ -1,7 +1,7 @@ /** * Actions related to the UI state of challenge details page. */ - +import _ from 'lodash'; import { createActions } from 'redux-actions'; /** @@ -62,6 +62,15 @@ function toggleCheckpointFeedback(id, open) { return { id, open }; } +/** + * Creates action that toggle the submission testcase.. + * @param {Number} index of submission testcase. + * @return {Action} + */ +function toggleSubmissionTestCase(index) { + return index; +} + export default createActions({ PAGE: { CHALLENGE_DETAILS: { @@ -70,6 +79,8 @@ export default createActions({ TOGGLE_CHECKPOINT_FEEDBACK: toggleCheckpointFeedback, SUBMISSIONS: { TOGGLE_SUBMISSION_HISTORY: toggleSubmissionHistory, + TOGGLE_SUBMISSION_TESTCASE: toggleSubmissionTestCase, + CLEAR_SUBMISSION_TESTCASE_OPEN: _.identity, }, }, }, diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx new file mode 100644 index 0000000000..c2eefda850 --- /dev/null +++ b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx @@ -0,0 +1,178 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import _ from 'lodash'; +import React from 'react'; +import PT from 'prop-types'; +import moment from 'moment'; +import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; +import ArrowUp from 'assets/images/icon-arrow-up.svg'; +import ArrowDown from 'assets/images/icon-arrow-down.svg'; +import LoadingIndicator from 'components/LoadingIndicator'; + +import modal from './style.scss'; + +class SubmissionInformationModal extends React.Component { + constructor(props) { + super(props); + + this.getTestCaseOpen = this.getTestCaseOpen.bind(this); + this.getSubmissionBasicInfo = this.getSubmissionBasicInfo.bind(this); + this.getTestcases = this.getTestcases.bind(this); + } + + componentWillUnmount() { + const { clearTestcaseOpen } = this.props; + clearTestcaseOpen(); + } + + getTestCaseOpen(index) { + const { openTestcase } = this.props; + return openTestcase[index.toString()] || false; + } + + + getSubmissionBasicInfo() { + const { submission, submissionInformation } = this.props; + return _.find(submission.submissions, item => item.submissionId === submissionInformation.id); + } + + getTestcases() { + const { submissionInformation } = this.props; + + let list = []; + _.forEach(submissionInformation.review, (item) => { + if (_.has(item, 'metadata') && _.has(item.metadata, 'testcases') && item.metadata.testcases.length > 0) { + list = list.concat(item.metadata.testcases); + } + }); + + return list; + } + + /* eslint-disable class-methods-use-this */ + renderCase(key, value) { + return ( + + {key} + {value} + + ); + } + + render() { + const { + toggleTestcase, onClose, isLoadingSubmissionInformation, + submissionInformation, isReviewPhaseComplete, + } = this.props; + const submissionBasicInfo = isLoadingSubmissionInformation + ? null : this.getSubmissionBasicInfo(); + const testcases = isLoadingSubmissionInformation ? [] : this.getTestcases(); + + return ( + onClose(false)}> + { + !isLoadingSubmissionInformation && ( + +
Advanced Details
+
+
+ Submission:  + {submissionInformation.id} +
+
+
+
Final Score
+
Provissional Score
+
Time
+
+
+
+ {(!submissionBasicInfo.finalScore && submissionBasicInfo.finalScore !== 0) || !isReviewPhaseComplete ? '-' : submissionBasicInfo.finalScore} +
+
+ {(!submissionBasicInfo.provisionalScore && submissionBasicInfo.provisionalScore !== 0) ? '-' : submissionBasicInfo.provisionalScore} +
+
+ {moment(submissionBasicInfo.submissionTime) + .format('DD MMM YYYY')} {moment(submissionBasicInfo.submissionTime) + .format('HH:mm:ss')} +
+
+
+
+ { + testcases.length > 0 && ( + +
+ Test cases executed +
+ { + _.map(testcases, (item, index) => ( +
+
+
Test case #{index + 1}
+ toggleTestcase(index)} role="button" tabIndex={0}> + { + this.getTestCaseOpen(index) ? : + } + +
+ { + _.keys(item).length > 0 && ( +
+ { + _.map(_.keys(item), (key, caseIndex) => ( +
+ { + this.renderCase(key, item[key]) + } +
+ )) + } +
+ ) + } +
+ )) + } +
+ ) + } +
+
+ onClose(false)}>Dismiss +
+
+
+ ) + } + { + isLoadingSubmissionInformation && ( + + ) + } +
+ ); + } +} + +SubmissionInformationModal.defaultProps = { + isLoadingSubmissionInformation: false, + submissionInformation: null, +}; + +SubmissionInformationModal.propTypes = { + isLoadingSubmissionInformation: PT.bool, + submissionInformation: PT.shape(), + onClose: PT.func.isRequired, + toggleTestcase: PT.func.isRequired, + openTestcase: PT.shape({}).isRequired, + clearTestcaseOpen: PT.func.isRequired, + submission: PT.shape().isRequired, + isReviewPhaseComplete: PT.bool.isRequired, +}; + +export default SubmissionInformationModal; diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/style.scss b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/style.scss new file mode 100644 index 0000000000..ade6604e7e --- /dev/null +++ b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/style.scss @@ -0,0 +1,197 @@ +@import "~styles/mixins"; + +.container { + @include roboto-regular; + + max-height: 75vh; + overflow: auto; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + background-color: $tc-white; + opacity: 1; + border-radius: 4px; + padding: 20px; + max-width: 100%; + box-shadow: 0 0 5px 0 silver, 0 0 1px 0 #d5d5d5; +} + +.testcase-items { + width: 100%; + display: flex; + flex-direction: column; + + .testcase-header { + display: flex; + justify-content: space-between; + background: #faf9fa; + height: 40px; + padding: 0 20px; + color: $tc-gray-50; + font-size: 16px; + font-weight: bold; + border-bottom: 1px solid $tc-gray-10; + align-items: center; + + span { + cursor: pointer; + } + } + + .testcase-grid { + width: 100%; + display: none; + + .testcase-row { + width: 100%; + display: flex; + height: 30px; + justify-content: space-between; + align-items: center; + color: $tc-gray-50; + font-size: 13px; + padding: 0 20px; + border-bottom: 1px solid $tc-gray-10; + font-weight: bold; + + span:last-child { + color: $tc-gray-90; + } + } + + &.active { + display: flex; + flex-direction: column; + } + } +} + +.submission-information-title { + font-size: 20px; + line-height: 24px; + color: $tc-gray-90; + text-transform: none; +} + +.submission-information-details { + margin: 10px 0; + width: 100%; + display: flex; + flex-direction: column; + + .submission-information-details-row1 { + display: flex; + justify-content: flex-start; + width: 100%; + height: 30px; + color: $tc-gray-50; + font-size: 13px; + align-items: center; + margin-bottom: 10px; + + .submission-information-details-title { + color: $tc-gray-90; + font-size: 16px; + font-weight: bold; + } + + .submission-information-details-id { + color: $tc-dark-blue; + font-size: 16px; + } + } + + .submission-information-details-row2 { + display: flex; + justify-content: space-between; + flex-direction: column; + width: 100%; + color: $tc-gray-50; + font-size: 13px; + + .details-header { + width: 100%; + display: flex; + justify-content: space-between; + background: #faf9fa; + height: 30px; + padding-right: 20px; + + .header-item { + display: flex; + justify-content: center; + align-items: center; + width: 43%; + font-weight: bold; + } + + .header-item:first-child { + width: 23%; + } + + .header-item:last-child { + justify-content: flex-end; + width: 33%; + } + } + + .details-grid { + width: 100%; + display: flex; + justify-content: space-between; + background-color: $tc-white; + border-bottom: 1px solid $tc-gray-10; + height: 30px; + padding-right: 20px; + align-items: center; + font-weight: bold; + + .details-item { + width: 43%; + display: flex; + justify-content: center; + align-items: center; + } + + .details-item:first-child { + width: 23%; + } + + .details-item:last-child { + justify-content: flex-end; + width: 33%; + min-width: 170px; + } + } + } +} + +.testcase-title { + font-size: 16px; + line-height: 24px; + color: $tc-gray-90; + text-transform: none; + font-weight: bold; + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 10px; + margin-top: 25px; +} + +.submission-information-buttons { + margin-top: 15px; + + .submission-information-button-close { + button { + @include roboto-regular; + + height: 45px; + width: 200px; + font-size: 16px; + background: #3b87f7; + border-radius: 5px; + } + } +} diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx index 4ae74f0d55..2e7f3d05b9 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx @@ -1,4 +1,5 @@ /* eslint jsx-a11y/no-static-element-interactions:0 */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ /** * SubmissionHistoryRow component. */ @@ -16,6 +17,9 @@ export default function SubmissionHistoryRow({ provisionalScore, submissionTime, isReviewPhaseComplete, + onShowPopup, + submissionId, + member, }) { return (
@@ -32,11 +36,25 @@ export default function SubmissionHistoryRow({ {(!provisionalScore && provisionalScore !== 0) ? '-' : provisionalScore}
-
+
{moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')}
+ { + isMM && ( +
+
onShowPopup(true, submissionId, member)} + > + View Details +
+
+ ) + }
); @@ -49,10 +67,13 @@ SubmissionHistoryRow.defaultProps = { }; SubmissionHistoryRow.propTypes = { + member: PT.string.isRequired, isMM: PT.bool.isRequired, submission: PT.number.isRequired, finalScore: PT.number, provisionalScore: PT.number, submissionTime: PT.string.isRequired, isReviewPhaseComplete: PT.bool, + submissionId: PT.string.isRequired, + onShowPopup: PT.func.isRequired, }; diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/style.scss b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/style.scss index c1af985423..75014ca1ad 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/style.scss +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/style.scss @@ -114,6 +114,18 @@ flex: 1; } } + + &.mm { + flex: 25; + } + } + + &.col-5 { + flex: 10; + color: #0a71e6; + cursor: pointer; + min-width: 83px; + outline: none; } } } diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx index 7987c3531a..6275ee55c0 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx @@ -14,8 +14,8 @@ import SubmissionHistoryRow from './SubmissionHistoryRow'; import './style.scss'; export default function SubmissionRow({ - isMM, openHistory, member, submissions, score, toggleHistory, colorStyle, isReviewPhaseComplete, - finalRank, provisionalRank, + isMM, openHistory, member, submissions, score, toggleHistory, colorStyle, + isReviewPhaseComplete, finalRank, provisionalRank, onShowPopup, }) { const { submissionTime, provisionalScore, @@ -89,9 +89,14 @@ export default function SubmissionRow({ Provisional -
+
Time
+ { + isMM && ( +
 
+ ) + }
{ @@ -102,6 +107,8 @@ export default function SubmissionRow({ submission={submissions.length - index} {...submissionHistory} key={submissionHistory.submissionId} + onShowPopup={onShowPopup} + member={member} /> )) } @@ -141,4 +148,5 @@ SubmissionRow.propTypes = { isReviewPhaseComplete: PT.bool, finalRank: PT.number, provisionalRank: PT.number, + onShowPopup: PT.func.isRequired, }; diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss b/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss index 45836a95b4..8dc28d04ae 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/style.scss @@ -167,6 +167,14 @@ } } } + + &.mm { + flex: 25; + } + } + + &.col-5 { + flex: 10; } } diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 2b19e755e0..bb501a2c32 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -16,11 +16,21 @@ import LoadingIndicator from 'components/LoadingIndicator'; import { goToLogin } from 'utils/tc'; import Lock from '../icons/lock.svg'; import SubmissionRow from './SubmissionRow'; +import SubmissionInformationModal from './SubmissionInformationModal'; import './style.scss'; const { getProvisionalScore, getFinalScore } = submissionUtils; class SubmissionsComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + isShowInformation: false, + memberOfModal: '', + }; + this.onHandleInformationPopup = this.onHandleInformationPopup.bind(this); + } + componentDidMount() { const { challenge, loadMMSubmissions, auth } = this.props; const isMM = challenge.subTrack.indexOf('MARATHON_MATCH') > -1; @@ -31,8 +41,21 @@ class SubmissionsComponent extends React.Component { return; } - if (isMM) { - loadMMSubmissions(challenge.id, challenge.registrants, auth.tokenV3); + if (isMM && _.has(challenge, 'submissions') && challenge.submissions.length > 0) { + const submitterIds = _.map(challenge.submissions, item => item.submitterId); + loadMMSubmissions(challenge.id, submitterIds, challenge.registrants, auth.tokenV3); + } + } + + onHandleInformationPopup(status, submissionId = null, member = '') { + const { loadSubmissionInformation, auth } = this.props; + this.setState({ + isShowInformation: status, + memberOfModal: member, + }); + + if (status) { + loadSubmissionInformation(submissionId, auth.tokenV3); } } @@ -42,6 +65,11 @@ class SubmissionsComponent extends React.Component { submissionHistoryOpen, mmSubmissions, loadingMMSubmissionsForChallengeId, + isLoadingSubmissionInformation, + submissionInformation, + toggleSubmissionTestcase, + submissionTestcaseOpen, + clearSubmissionTestcaseOpen, } = this.props; const { checkpoints, @@ -50,6 +78,11 @@ class SubmissionsComponent extends React.Component { allPhases, } = challenge; + const { isShowInformation, memberOfModal } = this.state; + + const modalSubmissionBasicInfo = () => _.find(mmSubmissions, + item => item.member === memberOfModal); + const renderSubmission = s => (
{ toggleSubmissionHistory(index); }} openHistory={(submissionHistoryOpen[index.toString()] || false)} + isLoadingSubmissionInformation={isLoadingSubmissionInformation} + submissionInformation={submissionInformation} + onShowPopup={this.onHandleInformationPopup} /> )) ) @@ -288,11 +324,30 @@ class SubmissionsComponent extends React.Component { )) ) } + { + isMM && isShowInformation && ( + + ) + }
); } } +SubmissionsComponent.defaultProps = { + isLoadingSubmissionInformation: false, + submissionInformation: null, +}; + SubmissionsComponent.propTypes = { auth: PT.shape().isRequired, challenge: PT.shape({ @@ -310,6 +365,12 @@ SubmissionsComponent.propTypes = { loadMMSubmissions: PT.func.isRequired, mmSubmissions: PT.arrayOf(PT.shape()).isRequired, loadingMMSubmissionsForChallengeId: PT.string.isRequired, + isLoadingSubmissionInformation: PT.bool, + submissionInformation: PT.shape(), + loadSubmissionInformation: PT.func.isRequired, + toggleSubmissionTestcase: PT.func.isRequired, + clearSubmissionTestcaseOpen: PT.func.isRequired, + submissionTestcaseOpen: PT.shape({}).isRequired, }; function mapDispatchToProps(dispatch) { @@ -317,12 +378,22 @@ function mapDispatchToProps(dispatch) { toggleSubmissionHistory: index => dispatch( challengeDetailsActions.page.challengeDetails.submissions.toggleSubmissionHistory(index), ), + toggleSubmissionTestcase: index => dispatch( + challengeDetailsActions.page.challengeDetails.submissions.toggleSubmissionTestcase(index), + ), + clearSubmissionTestcaseOpen: () => dispatch( + challengeDetailsActions.page.challengeDetails.submissions.clearSubmissionTestcaseOpen(), + ), }; } function mapStateToProps(state) { return { submissionHistoryOpen: state.page.challengeDetails.submissionHistoryOpen, + submissionTestcaseOpen: state.page.challengeDetails.submissionTestcaseOpen, + isLoadingSubmissionInformation: + Boolean(state.challenge.loadingSubmissionInformationForSubmissionId), + submissionInformation: state.challenge.submissionInformation, }; } diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index bc41f704de..58e4d2f551 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -238,6 +238,9 @@ class ChallengeDetailPageContainer extends React.Component { loadMMSubmissions, mmSubmissions, loadingMMSubmissionsForChallengeId, + isLoadingSubmissionInformation, + submissionInformation, + loadSubmissionInformation, } = this.props; const { @@ -386,6 +389,9 @@ class ChallengeDetailPageContainer extends React.Component { mmSubmissions={mmSubmissions} loadMMSubmissions={loadMMSubmissions} auth={auth} + isLoadingSubmissionInformation={isLoadingSubmissionInformation} + submssionInformation={submissionInformation} + loadSubmissionInformation={loadSubmissionInformation} /> ) } @@ -430,6 +436,8 @@ ChallengeDetailPageContainer.defaultProps = { isMenuOpened: false, loadingMMSubmissionsForChallengeId: '', mmSubmissions: [], + isLoadingSubmissionInformation: false, + submissionInformation: null, }; ChallengeDetailPageContainer.propTypes = { @@ -476,6 +484,9 @@ ChallengeDetailPageContainer.propTypes = { loadingMMSubmissionsForChallengeId: PT.string, mmSubmissions: PT.arrayOf(PT.shape()), loadMMSubmissions: PT.func.isRequired, + isLoadingSubmissionInformation: PT.bool, + submissionInformation: PT.shape(), + loadSubmissionInformation: PT.func.isRequired, }; function mapStateToProps(state, props) { @@ -511,6 +522,9 @@ function mapStateToProps(state, props) { unregistering: state.challenge.unregistering, isMenuOpened: !!state.topcoderHeader.openedMenu, loadingMMSubmissionsForChallengeId: state.challenge.loadingMMSubmissionsForChallengeId, + isLoadingSubmissionInformation: + Boolean(state.challenge.loadingSubmissionInformationForSubmissionId), + submissionInformation: state.challenge.submissionInformation, mmSubmissions: state.challenge.mmSubmissions, }; } @@ -609,10 +623,15 @@ const mapDispatchToProps = (dispatch) => { dispatch(a.updateChallengeInit(uuid)); dispatch(a.updateChallengeDone(uuid, challenge, tokenV3)); }, - loadMMSubmissions: (challengeId, registrants, tokenV3) => { + loadMMSubmissions: (challengeId, submitterIds, registrants, tokenV3) => { const a = actions.challenge; dispatch(a.getMmSubmissionsInit(challengeId)); - dispatch(a.getMmSubmissionsDone(challengeId, registrants, tokenV3)); + dispatch(a.getMmSubmissionsDone(challengeId, submitterIds, registrants, tokenV3)); + }, + loadSubmissionInformation: (submissionId, tokenV3) => { + const a = actions.challenge; + dispatch(a.getSubmissionInformationInit(submissionId)); + dispatch(a.getSubmissionInformationDone(submissionId, tokenV3)); }, }; }; diff --git a/src/shared/reducers/page/challenge-details.js b/src/shared/reducers/page/challenge-details.js index 46ac60cbef..2c19a68b8b 100644 --- a/src/shared/reducers/page/challenge-details.js +++ b/src/shared/reducers/page/challenge-details.js @@ -56,6 +56,27 @@ function toggleSubmissionHistory(state, { payload }) { return { ...state, submissionHistoryOpen: newSubmissionHistoryOpen }; } +/** + * Handler for open state of testcase of submission. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function toggleSubmissionTestcase(state, { payload }) { + const newSubmissionTestcaseOpen = _.clone(state.submissionTestcaseOpen); + newSubmissionTestcaseOpen[payload.toString()] = !newSubmissionTestcaseOpen[payload.toString()]; + return { ...state, submissionTestcaseOpen: newSubmissionTestcaseOpen }; +} + +/** + * Handler for clear state of testcase open of submission. + * @param {Object} state + * @return {Object} New state. + */ +function clearSubmissionTestcaseOpen(state) { + return { ...state, submissionTestcaseOpen: {} }; +} + /** * Creates a new reducer. * @param {Object} state Optional. Initial state. @@ -68,10 +89,13 @@ function create(state = {}) { [a.setSpecsTabState]: onSetSpecsTabState, [a.toggleCheckpointFeedback]: onToggleCheckpointFeedback, [a.submissions.toggleSubmissionHistory]: toggleSubmissionHistory, + [a.submissions.toggleSubmissionTestcase]: toggleSubmissionTestcase, + [a.submissions.clearSubmissionTestcaseOpen]: clearSubmissionTestcaseOpen, }, _.defaults(state, { checkpoints: {}, specsTabState: SPECS_TAB_STATES.VIEW, submissionHistoryOpen: {}, + submissionTestcaseOpen: {}, })); }