@@ -21,7 +21,7 @@ const ChallengeSpecTab = ({ challenge }) => (
(
-
+
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss
index 1aa3016218..7c13c67016 100644
--- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss
+++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss
@@ -1,6 +1,6 @@
@import "~styles/mixins";
-.comtainer {
+.container {
background: $tc-gray-neutral-dark;
width: 100%;
display: flex;
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx
index cc48ef7541..0013a94009 100644
--- a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx
+++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx
@@ -51,7 +51,7 @@ const ApplyTime = ({
disabled={!timeLeft || !openPositions}
onClick={() => onApply()}
>
- {hasApplied ? 'Manage Applications' : 'Apply for review'}
+ {hasApplied ? 'View Application' : 'Apply for review'}
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx
index c0a4db58c7..a258fa12a9 100644
--- a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx
+++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx
@@ -22,9 +22,9 @@ const { formatDuration } = time;
* @return {Object} The rendered React element
*/
const renderPhase = phase => (
-
+
- {phase.type}
+ {phase.name}
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx
index 2ffe5f2382..91709196b1 100644
--- a/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx
+++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx
@@ -31,7 +31,7 @@ const Header = ({
_.toString(app.handle) === _.toString(handle) && app.status !== 'Cancelled'))}
+ hasApplied={Boolean(_.find(details.applications, app => _.toString(app.handle) === _.toString(handle) && app.status !== 'CANCELLED'))}
startDate={details.startDate}
completed={details.openPositions === 0}
/>
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx
index 7e776f7147..8e63b3005c 100644
--- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx
+++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx
@@ -42,14 +42,14 @@ const ReviewOpportunityDetailsPage = ({
- {details.challenge.title}
+ {details.challenge.name}
Review Opportunities
- {details.challenge.type}
+ {typeof details.challenge.type === 'object' ? details.challenge.type.name : details.challenge.type}
@@ -71,7 +71,7 @@ const ReviewOpportunityDetailsPage = ({
>
REVIEW APPLICATIONS
{' '}
- {`(${details.applications ? details.applications.filter(app => app.status !== 'Cancelled').length : 0})`}
+ {`(${details.applications ? details.applications.filter(app => app.status !== 'CANCELLED').length : 0})`}
@@ -85,7 +85,7 @@ const ReviewOpportunityDetailsPage = ({
diff --git a/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx b/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx
index da0db39153..fcf827215a 100644
--- a/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx
+++ b/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx
@@ -27,46 +27,52 @@ const RatingsListModal = ({
onCancel,
submissionId,
challengeId,
- getReviewTypesList,
- getChallengeResources,
- getSubmissionInformation,
+ getSubmissionScores,
}) => {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(false);
- const enrichSources = useCallback(async (submissionReviews, reviewSummation) => {
- const reviewTypes = await getReviewTypesList();
- const resources = await getChallengeResources(challengeId);
-
- const finalReview = {
- reviewType: 'Final score',
- reviewer: '',
- score: reviewSummation ? reviewSummation.aggregateScore : 'N/A',
- isPassing: reviewSummation ? reviewSummation.isPassing : undefined,
- };
-
- return [...submissionReviews.map((review) => {
- const reviewType = reviewTypes.find(rt => rt.id === review.typeId);
- const reviewer = resources
- .find(resource => resource.memberHandle === review.reviewerId) || SystemReviewers.Default;
- return {
- ...review,
- reviewType: reviewType ? reviewType.name : '',
- reviewer,
- };
- }), finalReview];
- }, [challengeId, getReviewTypesList, getChallengeResources]);
-
- const getSubmission = useCallback(async () => {
- const submissionInfo = await getSubmissionInformation(submissionId);
- setReviews(await enrichSources(submissionInfo.review, submissionInfo.reviewSummation[0]));
- setLoading(false);
- }, [submissionId, getSubmissionInformation, enrichSources]);
+ const formatScoreValue = useCallback((value) => {
+ if (_.isNil(value)) {
+ return 'N/A';
+ }
+ if (Number.isFinite(value)) {
+ return Number.isInteger(value) ? `${value}` : value.toFixed(2);
+ }
+ const numeric = Number(value);
+ if (Number.isFinite(numeric)) {
+ return Number.isInteger(numeric) ? `${numeric}` : numeric.toFixed(2);
+ }
+ return `${value}`;
+ }, []);
+
+ const loadScores = useCallback(async () => {
+ if (!submissionId) {
+ setReviews([]);
+ return;
+ }
+ setLoading(true);
+ try {
+ const response = await getSubmissionScores(submissionId, challengeId);
+ const normalized = Array.isArray(response) ? response : [];
+ const decorated = normalized.map((entry, index) => ({
+ id: entry.id || `${submissionId}-${index}`,
+ reviewType: entry.label || entry.reviewType || 'Score',
+ reviewer: entry.reviewer || SystemReviewers.Default,
+ score: formatScoreValue(entry.score),
+ isPassing: typeof entry.isPassing === 'boolean' ? entry.isPassing : null,
+ }));
+ setReviews(decorated);
+ } catch (error) {
+ setReviews([]);
+ } finally {
+ setLoading(false);
+ }
+ }, [submissionId, challengeId, getSubmissionScores, formatScoreValue]);
useEffect(() => {
- setLoading(true);
- getSubmission();
- }, [submissionId, getSubmission]);
+ loadScores();
+ }, [loadScores]);
return (
onCancel()} theme={theme}>
@@ -131,18 +137,14 @@ RatingsListModal.defaultProps = {
onCancel: () => {},
submissionId: '',
challengeId: '',
- getReviewTypesList: _.noop,
- getChallengeResources: _.noop,
- getSubmissionInformation: _.noop,
+ getSubmissionScores: _.noop,
};
RatingsListModal.propTypes = {
onCancel: PropTypes.func,
submissionId: PropTypes.string,
challengeId: PropTypes.string,
- getReviewTypesList: PropTypes.func,
- getChallengeResources: PropTypes.func,
- getSubmissionInformation: PropTypes.func,
+ getSubmissionScores: PropTypes.func,
};
export default RatingsListModal;
diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx
index 9f3b771603..27b82fe3db 100644
--- a/src/shared/components/SubmissionManagement/Submission/index.jsx
+++ b/src/shared/components/SubmissionManagement/Submission/index.jsx
@@ -14,7 +14,7 @@
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
-import { COMPETITION_TRACKS, CHALLENGE_STATUS, safeForDownload } from 'utils/tc';
+import { CHALLENGE_STATUS, COMPETITION_TRACKS, safeForDownload } from 'utils/tc';
import PT from 'prop-types';
diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx
index 9ab0c2314a..6cbecd938e 100644
--- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx
+++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx
@@ -22,6 +22,7 @@ import { Link } from 'topcoder-react-utils';
import LeftArrow from 'assets/images/arrow-prev-green.svg';
import { PrimaryButton } from 'topcoder-react-ui-kit';
import { phaseEndDate } from 'utils/challenge-listing/helper';
+import { getTrackName } from 'utils/challenge';
import SubmissionsTable from '../SubmissionsTable';
import style from './styles.scss';
@@ -41,14 +42,13 @@ export default function SubmissionManagement(props) {
submissionPhaseStartDate,
onDownloadArtifacts,
getSubmissionArtifacts,
- getSubmissionInformation,
- getReviewTypesList,
- getChallengeResources,
+ getSubmissionScores,
} = props;
const { track } = challenge;
+ const trackName = getTrackName(track);
- const challengeType = track.toLowerCase();
+ const challengeType = (trackName || '').toLowerCase();
const isDesign = challengeType === 'design';
const isDevelop = challengeType === 'development';
@@ -57,6 +57,7 @@ export default function SubmissionManagement(props) {
.sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0];
const submissionPhase = challenge.phases.filter(p => p.name === 'Submission')[0];
const submissionEndDate = submissionPhase && phaseEndDate(submissionPhase);
+ const isSubmissionPhaseOpen = Boolean(submissionPhase && submissionPhase.isOpen);
const now = moment();
const end = moment(currentPhase && currentPhase.scheduledEndDate);
@@ -77,9 +78,7 @@ export default function SubmissionManagement(props) {
onShowDetails,
onDownloadArtifacts,
getSubmissionArtifacts,
- getSubmissionInformation,
- getReviewTypesList,
- getChallengeResources,
+ getSubmissionScores,
};
return (
@@ -129,7 +128,7 @@ export default function SubmissionManagement(props) {
}
{
- challenge.status !== 'Completed' ? (
+ challenge.status !== 'COMPLETED' ? (
Current Deadline Ends: {' '}
@@ -184,7 +183,7 @@ export default function SubmissionManagement(props) {
challenge={challenge}
submissionObjects={submissions}
showDetails={showDetails}
- track={track}
+ track={trackName}
status={challenge.status}
submissionPhaseStartDate={submissionPhaseStartDate}
{...componentConfig}
@@ -192,7 +191,7 @@ export default function SubmissionManagement(props) {
)
}
- {now.isBefore(submissionEndDate) && (
+ {isSubmissionPhaseOpen && now.isBefore(submissionEndDate) && (
{
@@ -159,11 +157,9 @@ export default function SubmissionsTable(props) {
setSubmissionId('');
setShowRatingsListModal(false);
}}
- getReviewTypesList={getReviewTypesList}
- getChallengeResources={getChallengeResources}
submissionId={submissionId}
challengeId={challenge.id}
- getSubmissionInformation={getSubmissionInformation}
+ getSubmissionScores={getSubmissionScores}
/>
)}
@@ -189,9 +185,7 @@ SubmissionsTable.defaultProps = {
getSubmissionArtifacts: _.noop,
onlineReviewUrl: '',
helpPageUrl: '',
- getReviewTypesList: _.noop,
- getChallengeResources: _.noop,
- getSubmissionInformation: _.noop,
+ getSubmissionScores: _.noop,
};
SubmissionsTable.propTypes = {
@@ -208,7 +202,5 @@ SubmissionsTable.propTypes = {
getSubmissionArtifacts: PT.func,
status: PT.string.isRequired,
submissionPhaseStartDate: PT.string.isRequired,
- getReviewTypesList: PT.func,
- getChallengeResources: PT.func,
- getSubmissionInformation: PT.func,
+ getSubmissionScores: PT.func,
};
diff --git a/src/shared/components/SubmissionPage/Submit/index.jsx b/src/shared/components/SubmissionPage/Submit/index.jsx
index 708182ea40..662fef81ed 100644
--- a/src/shared/components/SubmissionPage/Submit/index.jsx
+++ b/src/shared/components/SubmissionPage/Submit/index.jsx
@@ -78,13 +78,13 @@ class Submit extends React.Component {
// Submission type logic
if (checkpoint && checkpoint.isOpen) {
- subType = 'Checkpoint Submission';
+ subType = 'CHECKPOINT_SUBMISSION';
} else if (checkpoint && !checkpoint.isOpen && submission && submission.isOpen) {
- subType = 'Contest Submission';
+ subType = 'CONTEST_SUBMISSION';
} else if (finalFix && finalFix.isOpen) {
- subType = 'Studio Final Fix Submission';
+ subType = 'STUDIO_FINAL_FIX_SUBMISSION';
} else {
- subType = 'Contest Submission';
+ subType = 'CONTEST_SUBMISSION';
}
return subType;
diff --git a/src/shared/components/SubmissionPage/index.jsx b/src/shared/components/SubmissionPage/index.jsx
index fa4d339f5f..bd4dded61b 100644
--- a/src/shared/components/SubmissionPage/index.jsx
+++ b/src/shared/components/SubmissionPage/index.jsx
@@ -7,6 +7,8 @@
import React from 'react';
import PT from 'prop-types';
import _ from 'lodash';
+import { CHALLENGE_STATUS } from 'utils/tc';
+import { hasOpenSubmissionPhase } from 'utils/challengePhases';
import Header from './Header';
import Submit from './Submit';
import './styles.scss';
@@ -25,9 +27,8 @@ function SubmissionsPage(props) {
handle,
} = props;
- const submissionEnded = status === 'COMPLETED'
- || (!_.some(phases, { name: 'Submission', isOpen: true })
- && !_.some(phases, { name: 'Checkpoint Submission', isOpen: true }));
+ const submissionEnded = status === CHALLENGE_STATUS.COMPLETED
+ || !hasOpenSubmissionPhase(phases);
const hasFirstPlacement = !_.isEmpty(winners) && _.some(winners, { placement: 1, handle });
diff --git a/src/shared/components/TrackIcon/index.jsx b/src/shared/components/TrackIcon/index.jsx
index 33ccf15e72..fcf37779c9 100644
--- a/src/shared/components/TrackIcon/index.jsx
+++ b/src/shared/components/TrackIcon/index.jsx
@@ -13,7 +13,8 @@ export default function TrackIcon({
challengesUrl,
}) {
const TCO_URL = `${MAIN_URL}/tco`;
- const trackStyle = track.replace(' ', '-').toLowerCase();
+ const trackName = (track && typeof track === 'object') ? (track.name || '') : (track || '');
+ const trackStyle = trackName.replace(' ', '-').toLowerCase();
let abbreviationStyle = type.abbreviation;
if (['CH', 'F2F', 'TSK', 'MM'].indexOf(abbreviationStyle) < 0) {
abbreviationStyle = '';
diff --git a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
index 9c3e7cb300..a02f2177b5 100644
--- a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
+++ b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
@@ -16,6 +16,7 @@ import ArrowIcon from 'assets/images/ico-arrow-down.svg';
import CloseIcon from 'assets/images/icon-close-green.svg';
import SortIcon from 'assets/images/icon-sort-mobile.svg';
+import { getTypeName } from 'utils/challenge';
import style from './style.scss';
function getSelectorStyle(selectedView, currentView) {
@@ -57,7 +58,7 @@ export default function ChallengeViewSelector(props) {
const [isTabClosed, setIsTabClosed] = useState(true);
const [expanded, setExpanded] = useState(false);
const [selectedSortOption, setSelectedSortOption] = useState();
- const isF2F = type === 'First2Finish';
+ const isF2F = getTypeName({ type }) === 'First2Finish';
const isBugHunt = _.includes(tags, 'Bug Hunt');
const isDesign = trackLower === 'design';
@@ -134,7 +135,7 @@ export default function ChallengeViewSelector(props) {
const numOfSub = numOfSubmissions + (numOfCheckpointSubmissions || 0);
const forumId = _.get(challenge, 'legacy.forumId') || 0;
const discuss = _.get(challenge, 'discussions', []).filter(d => (
- d.type === 'challenge' && !_.isEmpty(d.url)
+ _.toLower(d.type) === 'challenge' && !_.isEmpty(d.url)
));
const roles = _.get(challenge, 'userDetails.roles') || [];
diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx
index a7426e0d7c..df568bb997 100644
--- a/src/shared/components/challenge-detail/Header/index.jsx
+++ b/src/shared/components/challenge-detail/Header/index.jsx
@@ -8,7 +8,7 @@
import _ from 'lodash';
import moment from 'moment';
import 'moment-duration-format';
-import { isMM } from 'utils/challenge';
+import { isMM, getTrackName, getTypeName } from 'utils/challenge';
import PT from 'prop-types';
import React, { useMemo } from 'react';
@@ -104,10 +104,16 @@ export default function ChallengeHeader(props) {
const sortedAllPhases = _.cloneDeep(allPhases)
.sort((a, b) => moment(phaseEndDate(a)).diff(phaseEndDate(b)));
- const placementPrizes = _.find(prizeSets, { type: 'placement' });
+ const placementPrizes = _.find(
+ prizeSets,
+ prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement',
+ );
const { prizes } = placementPrizes || [];
- const checkpointPrizes = _.find(prizeSets, { type: 'checkpoint' });
+ const checkpointPrizes = _.find(
+ prizeSets,
+ prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'checkpoint',
+ );
let numberOfCheckpointsPrizes = 0;
let topCheckPointPrize = 0;
if (!_.isEmpty(checkpointPrizes)) {
@@ -124,7 +130,7 @@ export default function ChallengeHeader(props) {
let registrationEnded = true;
const regPhase = phases && phases.registration;
- if (status !== 'Completed' && regPhase) {
+ if (status !== 'COMPLETED' && regPhase) {
registrationEnded = !regPhase.isOpen;
}
@@ -135,7 +141,9 @@ export default function ChallengeHeader(props) {
const currentPhases = allOpenPhases
.filter(p => !isRegistrationPhase(p))[0];
- const trackLower = track ? track.replace(' ', '-').toLowerCase() : 'design';
+ const trackName = getTrackName(track);
+ const typeName = getTypeName(type);
+ const trackLower = trackName ? trackName.replace(' ', '-').toLowerCase() : 'design';
const eventNames = (events || []).map((event => (event.eventName || '').toUpperCase()));
@@ -235,7 +243,7 @@ export default function ChallengeHeader(props) {
if (trackLower === 'quality-assurance') {
relevantPhases = _.filter(relevantPhases, p => !(p.name.toLowerCase().includes('specification submission') || p.name.toLowerCase().includes('specification review')));
}
- if (type === 'First2Finish' && status === 'Completed') {
+ if (typeName === 'First2Finish' && status === 'COMPLETED') {
const phases2 = allPhases.filter(p => p.name === 'Iterative Review' && !p.isOpen);
const endPhaseDate = Math.max(...phases2.map(d => phaseEndDate(d)));
relevantPhases = _.filter(relevantPhases, p => (p.name.toLowerCase().includes('registration')
@@ -319,7 +327,7 @@ export default function ChallengeHeader(props) {
Submit a solution
{
- track === COMPETITION_TRACKS.DES && hasRegistered && !unregistering
+ trackName === COMPETITION_TRACKS.DES && hasRegistered && !unregistering
&& hasSubmissions && (
{
_.each(entry.submissions, (sub) => {
- dates.push(sub.created || null);
+ dates.push(sub.created || sub.createdAt || null);
flatData.push({
..._.omit(entry, ['submissions']),
submissionCount: _.get(entry, 'submissions.length', 0),
@@ -40,7 +40,7 @@ export default function Graph({ statisticsData, baseline, awardLine }) {
color = data.ratingColor || getRatingColor(data.rating || 0);
}
return {
- x: moment(data.created).valueOf(),
+ x: moment(data.created || data.createdAt).valueOf(),
y: _.max([0, data.score ? (parseFloat(data.score)) : 0]),
name: data.handle,
color,
@@ -160,7 +160,7 @@ export default function Graph({ statisticsData, baseline, awardLine }) {
${currentPointer.customData.submissionCount} submissions
Score: ${this.y}
- Submitted: ${moment(currentPointer.customData.created).format('MM/DD/YYYY')}
+ Submitted: ${moment(currentPointer.customData.created || currentPointer.customData.createdAt).format('MM/DD/YYYY')}
`;
return str;
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
index 71ca8c6fea..821ad64351 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
@@ -7,6 +7,7 @@ import PT from 'prop-types';
import _ from 'lodash';
import cn from 'classnames';
import sortList from 'utils/challenge-detail/sort';
+import { getSubmissionStatus } from 'utils/challenge-detail/submission-status';
import Tooltip from 'components/Tooltip';
import IconClose from 'assets/images/icon-close-green.svg';
@@ -145,6 +146,9 @@ class SubmissionsDetailView extends React.Component {
const { onCancel, submission, onSortChange } = this.props;
let { finalScore } = submission;
const { sortedSubmissions } = this.state;
+ const { isAccepted } = getSubmissionStatus(submission);
+ const finalStatusStyleName = isAccepted ? 'status-complete' : 'status-in-queue';
+ const finalStatusLabel = isAccepted ? 'Complete' : 'In Queue';
const { field, sort } = this.getSubmissionsSortParam();
const revertSort = (sort === 'desc') ? 'asc' : 'desc';
@@ -373,9 +377,7 @@ class SubmissionsDetailView extends React.Component {
Status
- {submission.provisionalScoringIsCompleted ? (
- Complete
- ) : (In Queue)}
+ {finalStatusLabel}
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index 368e00cf1e..3a86b25541 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -10,6 +10,7 @@ import { PrimaryButton, Modal } from 'topcoder-react-ui-kit';
import PT from 'prop-types';
import { services } from 'topcoder-react-lib';
import sortList from 'utils/challenge-detail/sort';
+import { getSubmissionStatus } from 'utils/challenge-detail/submission-status';
import IconClose from 'assets/images/icon-close-green.svg';
import DateSortIcon from 'assets/images/icon-date-sort.svg';
@@ -25,6 +26,43 @@ import style from './styles.scss';
const { getService } = services.submissions;
+const collectReviewSummations = (submission) => {
+ const summations = [];
+ if (!submission) {
+ return summations;
+ }
+ if (Array.isArray(submission.reviewSummations)) {
+ summations.push(...submission.reviewSummations);
+ }
+ if (Array.isArray(submission.reviewSummation)) {
+ summations.push(...submission.reviewSummation);
+ }
+ return summations;
+};
+
+const getReviewSummationSubmissionId = (submission) => {
+ const summations = collectReviewSummations(submission);
+ const match = _.find(summations, s => s && !_.isNil(s.submissionId));
+ if (!match) {
+ return null;
+ }
+ return `${match.submissionId}`;
+};
+
+const getDisplaySubmissionId = (submission) => {
+ const fromSummation = getReviewSummationSubmissionId(submission);
+ if (fromSummation) {
+ return fromSummation;
+ }
+ if (submission && !_.isNil(submission.submissionId)) {
+ return `${submission.submissionId}`;
+ }
+ if (submission && !_.isNil(submission.id)) {
+ return `${submission.id}`;
+ }
+ return '';
+};
+
class SubmissionsListView extends React.Component {
constructor(props) {
@@ -100,16 +138,27 @@ class SubmissionsListView extends React.Component {
return sortList(submissions, field, sort, (a, b) => {
let valueA = 0;
let valueB = 0;
- const valueIsString = false;
+ let valueIsString = false;
switch (field) {
case 'Submission ID': {
- valueA = a.id;
- valueB = b.id;
+ const idA = getDisplaySubmissionId(a);
+ const idB = getDisplaySubmissionId(b);
+ const numericA = Number(idA);
+ const numericB = Number(idB);
+ const useNumericSort = idA !== '' && idB !== '' && _.isFinite(numericA) && _.isFinite(numericB);
+ if (useNumericSort) {
+ valueA = numericA;
+ valueB = numericB;
+ } else {
+ valueA = idA;
+ valueB = idB;
+ valueIsString = true;
+ }
break;
}
case 'Status': {
- valueA = a.provisionalScoringIsCompleted;
- valueB = b.provisionalScoringIsCompleted;
+ valueA = getSubmissionStatus(a).isAccepted ? 1 : 0;
+ valueB = getSubmissionStatus(b).isAccepted ? 1 : 0;
break;
}
case 'Final': {
@@ -391,8 +440,15 @@ class SubmissionsListView extends React.Component {
} else {
provisionalScore = 'N/A';
}
+ const { isAccepted } = getSubmissionStatus(mySubmission);
+ const statusStyleName = isAccepted ? 'accepted' : 'queue';
+ const statusLabel = isAccepted ? 'Accepted' : 'In Queue';
+ const displaySubmissionId = getDisplaySubmissionId(mySubmission);
return (
-
+
Submission Id
- {mySubmission.id}
+ {displaySubmissionId}
Status
- {mySubmission.provisionalScoringIsCompleted ? (
- Accepted
- ) : In Queue}
+ {statusLabel}
diff --git a/src/shared/components/challenge-detail/MySubmissions/index.jsx b/src/shared/components/challenge-detail/MySubmissions/index.jsx
index 1cf44d994e..d8ba171f95 100644
--- a/src/shared/components/challenge-detail/MySubmissions/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/index.jsx
@@ -197,7 +197,10 @@ MySubmissionsView.propTypes = {
submissionEnded: PT.bool.isRequired,
isMM: PT.bool.isRequired,
isLegacyMM: PT.bool.isRequired,
- loadingMMSubmissionsForChallengeId: PT.string.isRequired,
+ loadingMMSubmissionsForChallengeId: PT.oneOfType([
+ PT.string,
+ PT.oneOf([null]),
+ ]).isRequired,
auth: PT.shape().isRequired,
loadMMSubmissions: PT.func.isRequired,
mySubmissions: PT.arrayOf(PT.shape()).isRequired,
diff --git a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx
index 3bdeb882f6..b369983490 100644
--- a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx
+++ b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx
@@ -83,7 +83,7 @@ export default function ChallengesCard({
- {challenge.status === 'Active' ? 'Ends ' : 'Ended '}
+ {challenge.status === 'ACTIVE' ? 'Ends ' : 'Ended '}
{getEndDate(challenge)}
diff --git a/src/shared/components/challenge-detail/Registrants/index.jsx b/src/shared/components/challenge-detail/Registrants/index.jsx
index 5c21256bb1..0e90622b41 100644
--- a/src/shared/components/challenge-detail/Registrants/index.jsx
+++ b/src/shared/components/challenge-detail/Registrants/index.jsx
@@ -9,6 +9,7 @@ import moment from 'moment';
import _ from 'lodash';
import cn from 'classnames';
import { getRatingLevel } from 'utils/tc';
+import { getTrackName } from 'utils/challenge';
import sortList from 'utils/challenge-detail/sort';
import DateSortIcon from 'assets/images/icon-date-sort.svg';
@@ -286,9 +287,12 @@ export default class Registrants extends React.Component {
const { field, sort } = this.getRegistrantsSortParam();
const revertSort = (sort === 'desc') ? 'asc' : 'desc';
- const isDesign = track.toLowerCase() === 'design';
+ const isDesign = (getTrackName(track) || '').toLowerCase() === 'design';
- const placementPrizes = _.find(prizeSets, { type: 'placement' });
+ const placementPrizes = _.find(
+ prizeSets,
+ prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement',
+ );
const { prizes } = placementPrizes || [];
const checkpoints = challenge.checkpoints || [];
diff --git a/src/shared/components/challenge-detail/Specification/index.jsx b/src/shared/components/challenge-detail/Specification/index.jsx
index d6319fd682..60516359dd 100644
--- a/src/shared/components/challenge-detail/Specification/index.jsx
+++ b/src/shared/components/challenge-detail/Specification/index.jsx
@@ -11,7 +11,7 @@ import ToolbarConnector from 'components/Editor/Connector';
import React from 'react';
import Sticky from 'react-stickynode';
import { config } from 'topcoder-react-utils';
-import { isMM } from 'utils/challenge';
+import { isMM, getTrackName } from 'utils/challenge';
import PT from 'prop-types';
import { DangerButton } from 'topcoder-react-ui-kit';
@@ -83,12 +83,13 @@ export default function ChallengeDetailsView(props) {
}
const discuss = _.get(challenge, 'discussions', []).filter(d => (
- d.type === 'challenge' && !_.isEmpty(d.url)
+ _.toLower(d.type) === 'challenge' && !_.isEmpty(d.url)
));
let forumLink = '';
if (forumId > 0) {
- forumLink = track.toLowerCase() === 'design'
+ const trackName = (getTrackName(track) || '').toLowerCase();
+ forumLink = trackName === 'design'
? `/?module=ThreadList&forumID=${forumId}`
: `/?module=Category&categoryID=${forumId}`;
}
@@ -108,7 +109,7 @@ export default function ChallengeDetailsView(props) {
}
let accentedStyle = '';
- switch (track.toLowerCase()) {
+ switch ((getTrackName(track) || '').toLowerCase()) {
case 'design':
accentedStyle = 'challenge-specs-design';
break;
@@ -187,7 +188,7 @@ export default function ChallengeDetailsView(props) {
{
- track.toLowerCase() !== 'design'
+ (getTrackName(track) || '').toLowerCase() !== 'design'
? (
{
@@ -366,8 +367,8 @@ export default function ChallengeDetailsView(props) {
legacyId={legacyId}
forumLink={forumLink}
discuss={discuss}
- isDesign={track.toLowerCase() === 'design'}
- isDevelop={track.toLowerCase() === 'development'}
+ isDesign={(getTrackName(track) || '').toLowerCase() === 'design'}
+ isDevelop={(getTrackName(track) || '').toLowerCase() === 'development'}
eventDetail={_.isEmpty(events) ? null : events[0]}
isMM={isMM(challenge)}
terms={terms}
@@ -417,7 +418,7 @@ ChallengeDetailsView.propTypes = {
forumId: PT.number,
selfService: PT.bool,
}),
- track: PT.string.isRequired,
+ track: PT.oneOfType([PT.string, PT.shape()]).isRequired,
legacyId: PT.oneOfType([PT.string, PT.number]),
groups: PT.any,
reviewType: PT.string,
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 45d244cac8..54b7289dda 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
@@ -36,26 +36,40 @@ export default function SubmissionHistoryRow({
}) {
// todo: hide download button until update submissions API
const hideDownloadForMMRDM = true;
+ const parseScore = (value) => {
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+ };
+ const provisionalScoreValue = parseScore(provisionalScore);
+ const finalScoreValue = parseScore(finalScore);
+ const submissionMoment = submissionTime ? moment(submissionTime) : null;
+ const submissionTimeDisplay = submissionMoment
+ ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}`
+ : 'N/A';
const getInitialReviewResult = () => {
- if (provisionalScore && provisionalScore < 0) return ;
+ if (status === 'failed') return ;
+ if (status === 'in-review') return ;
+ if (status === 'queued') return ;
+ if (provisionalScoreValue === null) return 'N/A';
+ if (provisionalScoreValue < 0) return ;
switch (status) {
case 'completed':
- return provisionalScore;
- case 'in-review':
- return ;
- case 'queued':
- return ;
- case 'failed':
- return ;
+ return provisionalScoreValue;
default:
- return provisionalScore === '-' ? 'N/A' : provisionalScore;
+ return provisionalScoreValue;
}
};
const getFinalScore = () => {
- if (isMM && finalScore && finalScore > -1 && isReviewPhaseComplete) {
- return finalScore;
+ if (!isReviewPhaseComplete) {
+ return 'N/A';
+ }
+ if (finalScoreValue === null) {
+ return 'N/A';
}
- return 'N/A';
+ if (finalScoreValue < 0) {
+ return 0;
+ }
+ return finalScoreValue;
};
return (
@@ -80,7 +94,7 @@ export default function SubmissionHistoryRow({
TIME
- {moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')}
+ {submissionTimeDisplay}
{
@@ -129,13 +143,18 @@ SubmissionHistoryRow.propTypes = {
finalScore: PT.oneOfType([
PT.number,
PT.string,
+ PT.oneOf([null]),
]),
status: PT.string.isRequired,
provisionalScore: PT.oneOfType([
PT.number,
PT.string,
+ PT.oneOf([null]),
]),
- submissionTime: PT.string.isRequired,
+ submissionTime: PT.oneOfType([
+ PT.string,
+ PT.oneOf([null]),
+ ]).isRequired,
challengeStatus: PT.string.isRequired,
isReviewPhaseComplete: PT.bool,
auth: PT.shape().isRequired,
diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
index b2fc9cca9b..9ee00ea4e1 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
@@ -19,46 +19,78 @@ import SubmissionHistoryRow from './SubmissionHistoryRow';
import style from './style.scss';
export default function SubmissionRow({
- isMM, isRDM, openHistory, member, submissions, score, toggleHistory, challengeStatus,
+ isMM, isRDM, openHistory, member, submissions, toggleHistory, challengeStatus,
isReviewPhaseComplete, finalRank, provisionalRank, onShowPopup, rating, viewAsTable,
numWinners, auth, isLoggedIn,
}) {
+ const submissionList = Array.isArray(submissions) ? submissions : [];
+ const latestSubmission = submissionList[0] || {};
const {
- submissionTime, provisionalScore, status, submissionId,
- } = submissions[0];
- let { finalScore } = submissions[0];
- finalScore = (!finalScore && finalScore < 0) || !isReviewPhaseComplete ? '-' : finalScore;
- let initialScore;
- if (provisionalScore >= 0 || provisionalScore === -1) {
- initialScore = provisionalScore;
- }
+ status,
+ submissionId,
+ submissionTime,
+ } = latestSubmission;
+
+ const parseScore = (value) => {
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+ };
+
+ const provisionalScore = parseScore(_.get(latestSubmission, 'provisionalScore'));
+ const finalScore = parseScore(_.get(latestSubmission, 'finalScore'));
const getInitialReviewResult = () => {
- const s = isMM ? _.get(score, 'provisional', initialScore) : initialScore;
- if (s && s < 0) return ;
- switch (status) {
- case 'completed':
- return s;
- case 'in-review':
- return ;
- case 'queued':
- return ;
- case 'failed':
- return ;
- default:
- return s;
+ if (status === 'failed') {
+ return ;
+ }
+ if (status === 'in-review') {
+ return ;
+ }
+ if (status === 'queued') {
+ return ;
+ }
+ if (_.isNil(provisionalScore)) {
+ return 'N/A';
+ }
+ if (provisionalScore < 0) {
+ return ;
}
+ return provisionalScore;
};
const getFinalReviewResult = () => {
- const s = isMM && isReviewPhaseComplete ? _.get(score, 'final', finalScore) : finalScore;
- if (isReviewPhaseComplete) {
- if (s && s < 0) return 0;
- return s;
+ if (!isReviewPhaseComplete) {
+ return 'N/A';
}
- return 'N/A';
+ if (_.isNil(finalScore)) {
+ return 'N/A';
+ }
+ if (finalScore < 0) {
+ return 0;
+ }
+ return finalScore;
};
+ const initialReviewResult = getInitialReviewResult();
+ const finalReviewResult = getFinalReviewResult();
+
+ const submissionMoment = submissionTime ? moment(submissionTime) : null;
+ const submissionDateDisplay = submissionMoment
+ ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}`
+ : 'N/A';
+
+ const finalRankDisplay = (isReviewPhaseComplete && _.isFinite(finalRank)) ? finalRank : 'N/A';
+ const provisionalRankDisplay = _.isFinite(provisionalRank) ? provisionalRank : 'N/A';
+ const ratingDisplay = _.isFinite(rating) ? rating : '-';
+ const ratingLevelStyle = `col level-${getRatingLevel(rating)}`;
+ const memberHandle = member || '';
+ const memberDisplay = memberHandle || '-';
+ const memberProfileUrl = memberHandle ? `${window.origin}/members/${memberHandle}` : null;
+ const memberLinkTarget = `${_.includes(window.origin, 'www') ? '_self' : '_blank'}`;
+ const memberForHistory = memberHandle || memberDisplay;
+ const latestSubmissionId = submissionId || 'N/A';
+ const submissionCount = submissionList.length;
+
return (
@@ -67,14 +99,12 @@ export default function SubmissionRow({
FINAL RANK
- {
- isReviewPhaseComplete ? finalRank || 'N/A' : 'N/A'
- }
+ {finalRankDisplay}
PROVISIONAL RANK
- { provisionalRank || 'N/A' }
+ { provisionalRankDisplay }
@@ -82,37 +112,43 @@ export default function SubmissionRow({
}
RATING
-
- {rating || '-'}
+
+ {ratingDisplay}
USERNAME
-
- {member || '-'}
-
+ {
+ memberProfileUrl ? (
+
+ {memberDisplay}
+
+ ) : (
+ {memberDisplay}
+ )
+ }
FINAL SCORE
- {getFinalReviewResult()}
+ {finalReviewResult}
PROVISIONAL SCORE
- {getInitialReviewResult() ? getInitialReviewResult() : 'N/A'}
+ {initialReviewResult}
SUBMISSION DATE
- {moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')}
+ {submissionDateDisplay}
@@ -123,7 +159,7 @@ export default function SubmissionRow({
>
History (
- {submissions.length}
+ {submissionCount}
)
@@ -143,7 +179,7 @@ export default function SubmissionRow({
- Latest Submission: {submissionId}
+ Latest Submission: {latestSubmissionId}
@@ -182,17 +218,17 @@ export default function SubmissionRow({
{
- submissions.map((submissionHistory, index) => (
+ submissionList.map((submissionHistory, index) => (
{},
- score: {},
isReviewPhaseComplete: false,
finalRank: null,
provisionalRank: null,
@@ -234,29 +269,20 @@ SubmissionRow.propTypes = {
provisionalScore: PT.oneOfType([
PT.number,
PT.string,
+ PT.oneOf([null]),
]),
finalScore: PT.oneOfType([
PT.number,
PT.string,
- ]),
- initialScore: PT.oneOfType([
- PT.number,
- PT.string,
+ PT.oneOf([null]),
]),
status: PT.string.isRequired,
submissionId: PT.string.isRequired,
- submissionTime: PT.string.isRequired,
- })).isRequired,
- score: PT.shape({
- final: PT.oneOfType([
- PT.number,
- PT.string,
- ]),
- provisional: PT.oneOfType([
- PT.number,
+ submissionTime: PT.oneOfType([
PT.string,
- ]),
- }),
+ PT.oneOf([null]),
+ ]).isRequired,
+ })).isRequired,
rating: PT.number,
toggleHistory: PT.func,
isReviewPhaseComplete: PT.bool,
diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx
index 377259daaa..796dd3a0db 100644
--- a/src/shared/components/challenge-detail/Submissions/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/index.jsx
@@ -6,7 +6,12 @@
import React from 'react';
import PT from 'prop-types';
import moment from 'moment';
-import { isMM as checkIsMM, isRDM as checkIsRDM } from 'utils/challenge';
+import {
+ isMM as checkIsMM,
+ isRDM as checkIsRDM,
+ getTrackName,
+ getTypeName,
+} from 'utils/challenge';
import _ from 'lodash';
import { connect } from 'react-redux';
import { config } from 'topcoder-react-utils';
@@ -118,11 +123,11 @@ class SubmissionsComponent extends React.Component {
let score = 'N/A';
const { challenge } = this.props;
if (!_.isEmpty(submission.review)
- && !_.isEmpty(submission.review[0])
- && submission.review[0].score
- && (challenge.status === 'Completed'
+ && !_.isEmpty(submission)
+ && submission.initialScore
+ && (challenge.status === 'COMPLETED'
|| (_.includes(challenge.tags, 'Innovation Challenge') && _.find(challenge.metadata, { name: 'show_data_dashboard' })))) {
- score = Number(submission.review[0].score).toFixed(2);
+ score = Number(submission.initialScore).toFixed(2);
}
return score;
}
@@ -175,7 +180,9 @@ class SubmissionsComponent extends React.Component {
updateSortedSubmissions() {
const isMM = this.isMM();
const { submissions, mmSubmissions } = this.props;
- const sortedSubmissions = _.cloneDeep(isMM ? mmSubmissions : submissions);
+ const source = isMM ? mmSubmissions : submissions;
+ const sourceList = Array.isArray(source) ? source : [];
+ const sortedSubmissions = _.cloneDeep(sourceList);
this.sortSubmissions(sortedSubmissions);
this.setState({ sortedSubmissions });
}
@@ -185,16 +192,36 @@ class SubmissionsComponent extends React.Component {
* @param {Array} submissions array of submission
*/
sortSubmissions(submissions) {
+ if (!Array.isArray(submissions) || !submissions.length) {
+ return;
+ }
const isMM = this.isMM();
const isReviewPhaseComplete = this.checkIsReviewPhaseComplete();
const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete);
let isHaveFinalScore = false;
- if (field === 'Initial Score' || 'Final Score') {
+ if (field === 'Initial Score' || field === 'Final Score') {
isHaveFinalScore = _.some(submissions, s => !_.isNil(
- s.reviewSummation && s.reviewSummation[0].aggregateScore,
+ s.review && s.finalScore,
));
}
- return sortList(submissions, field, sort, (a, b) => {
+ const toSubmissionTime = (entry) => {
+ const latest = _.get(entry, ['submissions', 0]);
+ if (!latest) {
+ return null;
+ }
+ const { submissionTime } = latest;
+ if (!submissionTime) {
+ return null;
+ }
+ const timestamp = new Date(submissionTime).getTime();
+ return Number.isFinite(timestamp) ? timestamp : null;
+ };
+ const toRankValue = rank => (_.isFinite(rank) ? rank : Number.MAX_SAFE_INTEGER);
+ const toScoreValue = (score) => {
+ const numeric = Number(score);
+ return Number.isFinite(numeric) ? numeric : null;
+ };
+ sortList(submissions, field, sort, (a, b) => {
let valueA = 0;
let valueB = 0;
let valueIsString = false;
@@ -222,48 +249,50 @@ class SubmissionsComponent extends React.Component {
break;
}
case 'Time':
- valueA = new Date(a.submissions && a.submissions[0].submissionTime);
- valueB = new Date(b.submissions && b.submissions[0].submissionTime);
+ valueA = toSubmissionTime(a);
+ valueB = toSubmissionTime(b);
break;
case 'Submission Date': {
- valueA = new Date(a.created);
- valueB = new Date(b.created);
+ const createdA = a.created || a.createdAt;
+ const createdB = b.created || b.createdAt;
+ valueA = createdA ? new Date(createdA).getTime() : null;
+ valueB = createdB ? new Date(createdB).getTime() : null;
break;
}
case 'Initial Score': {
if (isHaveFinalScore) {
- valueA = getFinalScore(a);
- valueB = getFinalScore(b);
+ valueA = !_.isEmpty(a.review) && a.finalScore;
+ valueB = !_.isEmpty(b.review) && b.finalScore;
} else if (valueA.score || valueB.score) {
// Handle MM formatted scores in a code challenge (PS-295)
valueA = Number(valueA.score);
valueB = Number(valueB.score);
} else {
- valueA = !_.isEmpty(a.review) && a.review[0].score;
- valueB = !_.isEmpty(b.review) && b.review[0].score;
+ valueA = !_.isEmpty(a.review) && a.initialScore;
+ valueB = !_.isEmpty(b.review) && b.initialScore;
}
break;
}
case 'Final Rank': {
- if (this.checkIsReviewPhaseComplete()) {
- valueA = a.finalRank ? a.finalRank : 0;
- valueB = b.finalRank ? b.finalRank : 0;
+ if (isReviewPhaseComplete) {
+ valueA = toRankValue(_.get(a, 'finalRank'));
+ valueB = toRankValue(_.get(b, 'finalRank'));
}
break;
}
case 'Provisional Rank': {
- valueA = a.provisionalRank ? a.provisionalRank : 0;
- valueB = b.provisionalRank ? b.provisionalRank : 0;
+ valueA = toRankValue(_.get(a, 'provisionalRank'));
+ valueB = toRankValue(_.get(b, 'provisionalRank'));
break;
}
case 'Final Score': {
- valueA = getFinalScore(a);
- valueB = getFinalScore(b);
+ valueA = toScoreValue(getFinalScore(a));
+ valueB = toScoreValue(getFinalScore(b));
break;
}
case 'Provisional Score': {
- valueA = getProvisionalScore(a);
- valueB = getProvisionalScore(b);
+ valueA = toScoreValue(getProvisionalScore(a));
+ valueB = toScoreValue(getProvisionalScore(b));
break;
}
default:
@@ -284,7 +313,8 @@ class SubmissionsComponent extends React.Component {
isMM() {
const { challenge } = this.props;
- return challenge.track.toLowerCase() === 'data science' || checkIsMM(challenge);
+ const trackName = getTrackName(challenge);
+ return (trackName || '').toLowerCase() === 'data science' || checkIsMM(challenge);
}
/**
@@ -336,6 +366,8 @@ class SubmissionsComponent extends React.Component {
type,
tags,
} = challenge;
+ const trackName = getTrackName(track);
+ const typeName = getTypeName(type);
// todo: hide download button until update submissions API
const hideDownloadForMMRDM = true;
@@ -414,7 +446,7 @@ class SubmissionsComponent extends React.Component {
);
- const isF2F = type === 'First2Finish';
+ const isF2F = typeName === 'First2Finish';
const isBugHunt = _.includes(tags, 'Bug Hunt');
// copy colorStyle from registrants to submissions
@@ -434,7 +466,7 @@ class SubmissionsComponent extends React.Component {
}
});
- if (track.toLowerCase() === 'design') {
+ if ((trackName || '').toLowerCase() === 'design') {
return challenge.submissionViewable === 'true' ? (
@@ -902,7 +934,7 @@ class SubmissionsComponent extends React.Component {
{
!isMM && (
sortedSubmissions.map(s => (
-
+
{
!isF2F && !isBugHunt && (
@@ -927,7 +959,7 @@ class SubmissionsComponent extends React.Component {
SUBMISSION DATE
- {moment(s.created).format('MMM DD, YYYY HH:mm')}
+ {moment(s.created || s.createdAt).format('MMM DD, YYYY HH:mm')}
@@ -940,8 +972,8 @@ class SubmissionsComponent extends React.Component {
FINAL SCORE
{
- (s.reviewSummation && s.reviewSummation[0].aggregateScore && challenge.status === 'Completed')
- ? s.reviewSummation[0].aggregateScore.toFixed(2)
+ (s.review && s.finalScore && challenge.status === 'COMPLETED')
+ ? Number(s.finalScore).toFixed(2)
: 'N/A'
}
@@ -1012,7 +1044,10 @@ SubmissionsComponent.propTypes = {
submissionHistoryOpen: PT.shape({}).isRequired,
loadMMSubmissions: PT.func.isRequired,
mmSubmissions: PT.arrayOf(PT.shape()).isRequired,
- loadingMMSubmissionsForChallengeId: PT.string.isRequired,
+ loadingMMSubmissionsForChallengeId: PT.oneOfType([
+ PT.string,
+ PT.oneOf([null]),
+ ]).isRequired,
isLoadingSubmissionInformation: PT.bool,
submissionInformation: PT.shape(),
loadSubmissionInformation: PT.func.isRequired,
diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
index 503718e85e..73189a5914 100644
--- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
+++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
@@ -11,6 +11,7 @@ import {
getTimeLeft,
} from 'utils/challenge-detail/helper';
+import { getTypeName } from 'utils/challenge';
import ChallengeProgressBar from '../../ChallengeProgressBar';
import ProgressBarTooltip from '../../Tooltips/ProgressBarTooltip';
import UserAvatarTooltip from '../../Tooltips/UserAvatarTooltip';
@@ -232,14 +233,14 @@ export default function ChallengeStatus(props) {
.filter(p => p.name !== 'Registration' && p.isOpen)
.sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0];
- if (!statusPhase && type === 'First2Finish' && allPhases.length) {
+ if (!statusPhase && getTypeName({ type }) === 'First2Finish' && allPhases.length) {
statusPhase = _.clone(allPhases[0]);
statusPhase.name = 'Submission';
}
let phaseMessage = STALLED_MSG;
if (statusPhase) phaseMessage = statusPhase.name;
- else if (status === 'Draft') phaseMessage = DRAFT_MSG;
+ else if (status === 'DRAFT') phaseMessage = DRAFT_MSG;
const showRegisterInfo = false;
@@ -287,7 +288,7 @@ export default function ChallengeStatus(props) {
{
- status === 'Active' && statusPhase ? (
+ status === 'ACTIVE' && statusPhase ? (
diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx
index 3fb2d7b812..80803a7713 100644
--- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx
+++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx
@@ -42,6 +42,7 @@ function ChallengeCard({
id,
track,
} = challenge;
+ const trackName = (track && typeof track === 'object') ? (track.name || '') : track;
challenge.prize = challenge.prizes || [];
@@ -59,12 +60,12 @@ function ChallengeCard({
0 ? challenge.events[0].key : ''}
/>
@@ -84,7 +85,7 @@ function ChallengeCard({
- {challenge.status === 'Active' ? 'Ends ' : 'Ended '}
+ {challenge.status === 'ACTIVE' ? 'Ends ' : 'Ended '}
{getEndDate(challenge)}
{
diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
index 881f5e99b9..3fe0b2e9dc 100644
--- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
+++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
@@ -485,7 +485,7 @@ export default function FiltersPanel({
events: [],
endDateStart: null,
startDateEnd: null,
- status: 'Active',
+ status: 'ACTIVE',
reviewOpportunityTypes: _.keys(REVIEW_OPPORTUNITY_TYPES),
customDate: false,
recommended: false,
diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx
index 22a21d39bc..7021ad7994 100644
--- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx
+++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx
@@ -16,6 +16,7 @@ import {
import SortingSelectBar from 'components/SortingSelectBar';
import Waypoint from 'react-waypoint';
// import { challenge as challengeUtils } from 'topcoder-react-lib';
+import { getTypeName } from 'utils/challenge';
import CardPlaceholder from '../../placeholders/ChallengeCard';
import ChallengeCard from '../../ChallengeCard';
import NoRecommenderChallengeCard from '../../NoRecommenderChallengeCard';
@@ -85,7 +86,8 @@ export default function Bucket({
if (!_.includes(roles, 'administrator')) {
filteredChallenges = sortedChallenges.filter((ch) => {
- if (ch.type === 'Task'
+ const typeName = getTypeName(ch);
+ if (typeName === 'Task'
&& ch.task
&& ch.task.isTask
&& ch.task.isAssigned
@@ -158,7 +160,7 @@ export default function Bucket({
const cards = filteredChallenges.map(challenge => (
{
diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx
index 07cc41627d..b7c8d4912c 100644
--- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx
+++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx
@@ -1,21 +1,20 @@
/**
* The bucket for review opportunities.
*/
+import React from 'react';
import _ from 'lodash';
import PT from 'prop-types';
-import React from 'react';
-import Sort from 'utils/challenge-listing/sort';
-import { BUCKET_DATA } from 'utils/challenge-listing/buckets';
-import SortingSelectBar from 'components/SortingSelectBar';
import Waypoint from 'react-waypoint';
-import { challenge as challengeUtils } from 'topcoder-react-lib';
-import CardPlaceholder from '../../placeholders/ChallengeCard';
+import SortingSelectBar from 'components/SortingSelectBar';
+import { BUCKET_DATA } from 'utils/challenge-listing/buckets';
+import Sort from 'utils/challenge-listing/sort';
+import { getReviewOpportunitiesFilterFunction } from 'utils/reviewOpportunities';
+
import ReviewOpportunityCard from '../../ReviewOpportunityCard';
+import CardPlaceholder from '../../placeholders/ChallengeCard';
import './style.scss';
-const Filter = challengeUtils.filter;
-
const NO_RESULTS_MESSAGE = 'No challenges found';
const LOADING_MESSAGE = 'Loading Challenges';
@@ -49,7 +48,7 @@ export default function ReviewOpportunityBucket({
* which avoids reloading the review opportunities from server every time
* a filter is changed. */
const filteredOpportunities = sortedOpportunities.filter(
- Filter.getReviewOpportunitiesFilterFunction({
+ getReviewOpportunitiesFilterFunction({
...BUCKET_DATA[bucket].filter, // Default bucket filters from utils/buckets.js
...filterState, // User selected filters
}, challengeTypes),
@@ -59,7 +58,7 @@ export default function ReviewOpportunityBucket({
const cards = filteredOpportunities.map(item => (
{
@@ -142,7 +141,7 @@ export default function ReviewOpportunityBucket({
)
}
{
- loadMore && !loading && filterState.reviewOpportunityTypes.length ? (
+ loadMore && !loading ? (
) : null
}
diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx
index c71550717e..b0b9b86aee 100644
--- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx
+++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx
@@ -3,13 +3,13 @@
* information. Will be contained within a Bucket.
*/
import _ from 'lodash';
-import { Link } from 'topcoder-react-utils';
import moment from 'moment';
-import React, { useMemo } from 'react';
import PT from 'prop-types';
+import React, { useMemo } from 'react';
+import { Link } from 'topcoder-react-utils';
-import TrackIcon from 'components/TrackIcon';
import Tooltip from 'components/Tooltip';
+import TrackIcon from 'components/TrackIcon';
import { time } from 'topcoder-react-lib';
import { REVIEW_OPPORTUNITY_TYPES } from 'utils/tc';
@@ -18,8 +18,8 @@ import Tags from '../Tags';
import TrackAbbreviationTooltip from '../Tooltips/TrackAbbreviationTooltip';
-import SubmissionsIcon from '../Icons/SubmissionsIcon';
import OpenPositionsIcon from '../Icons/RegistrantsIcon';
+import SubmissionsIcon from '../Icons/SubmissionsIcon';
import './style.scss';
@@ -47,14 +47,16 @@ function ReviewOpportunityCard({
opportunity,
challengeType,
}) {
- const { challenge } = opportunity;
+ const { challengeData: challenge } = opportunity;
let tags = challenge.tags || challenge.technologies;
const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [
challenge.skills,
]);
tags = tags.filter(tag => tag.trim().length);
- const { track } = challenge.track;
+ const { track } = challenge;
const start = moment(opportunity.startDate);
+ const now = moment();
+ const isLate = now.isAfter(start);
return (
@@ -78,7 +80,7 @@ function ReviewOpportunityCard({
) /* END - DISABLED UNTIL REVIEW OPPORTUNITY RECEIVE UPDATE TO API V5 */ }
{challenge.title}
@@ -156,12 +158,15 @@ function ReviewOpportunityCard({
- Late by
- { start.isAfter() ? formatDuration(start.diff()) : ` ${formatDuration(-start.diff())}` }
+ {isLate ? 'Late by' : 'Time left'}
+ {isLate
+ ? formatDuration(now.diff(start))
+ : formatDuration(start.diff(now))
+ }
to Apply
diff --git a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx
index d79b811a07..a33c1943a4 100644
--- a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx
+++ b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx
@@ -13,6 +13,7 @@ import {
import { Link } from 'topcoder-react-utils';
import { COMPETITION_TRACKS } from 'utils/tc';
+import { getTrackName } from 'utils/challenge';
import './style.scss';
@@ -29,7 +30,7 @@ export default function Card({
} = challenge;
let TrackTag;
- switch (track.toLowerCase()) {
+ switch ((getTrackName(track) || '').toLowerCase()) {
case 'datasci':
case COMPETITION_TRACKS.DS:
TrackTag = DataScienceTrackTag;
diff --git a/src/shared/containers/Dashboard/ChallengesFeed.jsx b/src/shared/containers/Dashboard/ChallengesFeed.jsx
index 113aa5881c..b8ede649ca 100644
--- a/src/shared/containers/Dashboard/ChallengesFeed.jsx
+++ b/src/shared/containers/Dashboard/ChallengesFeed.jsx
@@ -23,7 +23,7 @@ class ChallengesFeedContainer extends React.Component {
perPage: excludeTags && excludeTags.length ? undefined : itemCount,
types: ['CH', 'F2F', 'MM'],
tracks,
- status: 'Active',
+ status: 'ACTIVE',
sortBy: 'updated',
sortOrder: 'desc',
isLightweight: true,
diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx
index bb40d2f79e..3b6ae8b17c 100644
--- a/src/shared/containers/ReviewOpportunityDetails.jsx
+++ b/src/shared/containers/ReviewOpportunityDetails.jsx
@@ -10,11 +10,12 @@ import { connect } from 'react-redux';
import { actions, errors } from 'topcoder-react-lib';
import LoadingIndicator from 'components/LoadingIndicator';
-import { activeRoleIds } from 'utils/reviewOpportunities';
import pageActions from 'actions/page/review-opportunity-details';
import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage';
import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad';
import termsActions from 'actions/terms';
+import { goToLogin } from 'utils/tc';
+import { logger } from 'tc-core-library-js';
const { fireErrorMessage } = errors;
@@ -25,6 +26,7 @@ class ReviewOpportunityDetailsContainer extends React.Component {
componentDidMount() {
const {
challengeId,
+ opportunityId,
details,
isLoadingDetails,
loadDetails,
@@ -32,19 +34,37 @@ class ReviewOpportunityDetailsContainer extends React.Component {
} = this.props;
if (!isLoadingDetails && !details) {
- loadDetails(challengeId, tokenV3);
+ loadDetails(challengeId, opportunityId, tokenV3);
} else if (details.challenge.id !== challengeId) {
- loadDetails(challengeId, tokenV3);
+ loadDetails(challengeId, opportunityId, tokenV3);
}
}
handleOnHeaderApply() {
const {
+ isLoggedIn,
+ isReviewer,
openTermsModal,
terms,
termsFailure,
toggleApplyModal,
} = this.props;
+
+ if (!isLoggedIn) {
+ goToLogin('community-app-main');
+ return;
+ }
+
+ if (!isReviewer) {
+ fireErrorMessage(
+ 'Permission Required',
+
+ You must have a reviewer role to apply for this review opportunity.
+ ,
+ );
+ return;
+ }
+
if (termsFailure) {
fireErrorMessage('Error Getting Terms Details', '');
return;
@@ -56,45 +76,28 @@ class ReviewOpportunityDetailsContainer extends React.Component {
}
}
- handleOnModalApply() {
+ async handleOnModalApply() {
const {
- cancelApplications,
challengeId,
- details,
- handle,
+ opportunityId,
loadDetails,
- selectedRoles,
submitApplications,
toggleApplyModal,
tokenV3,
} = this.props;
- const rolesToApply = [];
- const rolesToCancel = [];
-
- const previousRoles = activeRoleIds(details, handle);
+ try {
+ // Wait for the submit to finish (and succeed)
+ await submitApplications(opportunityId, tokenV3);
- previousRoles.forEach((id) => {
- if (!_.includes(selectedRoles, id)) {
- rolesToCancel.push(id);
- }
- });
-
- selectedRoles.forEach((id) => {
- if (!_.includes(previousRoles, id)) {
- rolesToApply.push(id);
- }
- });
+ toggleApplyModal();
- if (rolesToApply.length) {
- submitApplications(challengeId, rolesToApply, tokenV3);
- }
- if (rolesToCancel.length) {
- cancelApplications(challengeId, rolesToCancel, tokenV3);
+ await loadDetails(challengeId, opportunityId, tokenV3);
+ } catch (err) {
+ logger.error('submitApplications failed', err);
+ toggleApplyModal();
}
-
- toggleApplyModal();
- loadDetails(challengeId, tokenV3);
+ loadDetails(challengeId, opportunityId, tokenV3);
}
render() {
@@ -130,6 +133,8 @@ ReviewOpportunityDetailsContainer.defaultProps = {
termsFailure: false,
phasesExpanded: false,
tokenV3: null,
+ isLoggedIn: false,
+ isReviewer: false,
};
/**
@@ -140,6 +145,7 @@ ReviewOpportunityDetailsContainer.propTypes = {
authError: PT.bool,
cancelApplications: PT.func.isRequired,
challengeId: PT.string.isRequired,
+ opportunityId: PT.string.isRequired,
details: PT.shape(),
handle: PT.string.isRequired,
isLoadingDetails: PT.bool,
@@ -157,6 +163,8 @@ ReviewOpportunityDetailsContainer.propTypes = {
toggleRole: PT.func.isRequired,
onPhaseExpand: PT.func.isRequired,
tokenV3: PT.string,
+ isLoggedIn: PT.bool,
+ isReviewer: PT.bool,
};
/**
@@ -169,12 +177,14 @@ ReviewOpportunityDetailsContainer.propTypes = {
const mapStateToProps = (state, ownProps) => {
const api = state.reviewOpportunity;
const page = state.page.reviewOpportunityDetails;
+ const queryParams = new URLSearchParams(ownProps.location.search);
const { terms } = state;
return {
authError: api.authError,
applyModalOpened: page.applyModalOpened,
challengeId: String(ownProps.match.params.challengeId),
- details: api.details,
+ opportunityId: queryParams.get('opportunityId'),
+ details: page.details,
handle: state.auth.user ? state.auth.user.handle : '',
isLoadingDetails: api.isLoadingDetails,
phasesExpanded: page.phasesExpanded,
@@ -184,6 +194,8 @@ const mapStateToProps = (state, ownProps) => {
terms: terms.terms,
termsFailure: terms.getTermsFailure,
tokenV3: state.auth.tokenV3,
+ isLoggedIn: Boolean(state.auth.user),
+ isReviewer: _.includes((state.auth.user && state.auth.user.roles) || [], 'reviewer'),
};
};
@@ -201,9 +213,9 @@ function mapDispatchToProps(dispatch) {
dispatch(api.cancelApplicationsInit());
dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3));
},
- loadDetails: (challengeId, tokenV3) => {
- dispatch(api.getDetailsInit());
- dispatch(api.getDetailsDone(challengeId, tokenV3));
+ loadDetails: (challengeId, opportunityId, tokenV3) => {
+ dispatch(page.getDetailsInit());
+ return dispatch(page.getDetailsDone(challengeId, opportunityId, tokenV3));
},
onPhaseExpand: () => dispatch(page.togglePhasesExpand()),
openTermsModal: () => {
@@ -211,9 +223,9 @@ function mapDispatchToProps(dispatch) {
},
selectTab: tab => dispatch(page.selectTab(tab)),
setRoles: roles => dispatch(page.setRoles(roles)),
- submitApplications: (challengeId, roleIds, tokenV3) => {
- dispatch(api.submitApplicationsInit());
- dispatch(api.submitApplicationsDone(challengeId, roleIds, tokenV3));
+ submitApplications: (challengeId, tokenV3) => {
+ dispatch(page.submitApplicationsInit());
+ return dispatch(page.submitApplicationsDone(challengeId, tokenV3));
},
toggleApplyModal: () => {
dispatch(page.toggleApplyModal());
diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx
new file mode 100644
index 0000000000..7770912fa0
--- /dev/null
+++ b/src/shared/containers/SmartLooker.jsx
@@ -0,0 +1,182 @@
+/**
+ * SmartLooker bridges legacy usages to the new
+ * reports-api-v6 endpoints for the /community/statistics page. The reports
+ * service now mirrors the original Looker schemas, so we simply proxy data
+ * from the API to the existing Looker component.
+ */
+import React from 'react';
+import PT from 'prop-types';
+import { config } from 'topcoder-react-utils';
+import Looker from 'components/Looker';
+
+const LOOKER_TO_REPORTS_PATH = {
+ 1127: '/statistics/development/challenges-technology',
+ 1129: '/statistics/development/countries-represented',
+ 1130: '/statistics/development/first-place-wins',
+ 1131: '/statistics/development/prototype-wins',
+ 1132: '/statistics/development/code-wins',
+ 1133: '/statistics/development/f2f-wins',
+ 1135: '/statistics/design/first-place-by-country',
+ 1136: '/statistics/design/countries-represented',
+ 1138: '/statistics/design/ui-design-wins',
+ 1140: '/statistics/design/wireframe-wins',
+ 1141: '/statistics/design/f2f-wins',
+ 1146: '/statistics/general/copiloted-challenges',
+ 1148: '/statistics/general/countries-represented',
+ 1149: '/statistics/general/first-place-by-country',
+ 1150: '/statistics/general/reviews-by-member',
+ 1172: '/statistics/development/first-time-submitters',
+ 1178: '/statistics/design/first-time-submitters',
+ 1571: '/statistics/design/lux-first-place-wins',
+ 1572: '/statistics/design/lux-placements',
+ 1573: '/statistics/design/rux-first-place-wins',
+ 1574: '/statistics/design/rux-placements',
+ 1652: '/statistics/mm/top-rated',
+ 1653: '/statistics/srm/top-rated',
+ 1654: '/statistics/srm/competitions-count',
+ 1655: '/statistics/mm/competitions-count',
+ 1656: '/statistics/mm/top-10-finishes',
+ 1657: '/statistics/srm/country-ratings',
+ 1658: '/statistics/mm/country-ratings',
+ 1700: '/statistics/qa/wins',
+};
+
+function inferFromProps(property, render) {
+ if (property) {
+ const normalized = String(property).toLowerCase();
+ if (normalized === 'user.count') {
+ return { path: '/statistics/general/member-count' };
+ }
+ if (normalized === 'challenge.count') {
+ return { path: '/statistics/general/completed-challenges' };
+ }
+ if (
+ normalized === 'total'
+ || normalized.includes('total')
+ || normalized.includes('prize')
+ || normalized.includes('payment')
+ || normalized.includes('amount')
+ ) {
+ return {
+ path: '/statistics/general/total-prizes',
+ transform: data => ([{ [property]: data.total }]),
+ };
+ }
+ }
+
+ if (!property && render) {
+ try {
+ const source = String(render).replace(/&q;/g, '"').replace(/'/g, '"');
+ const matches = Array.from(
+ source.matchAll(/data\s*\[\s*0\s*]\s*\[\s*"([^"]+)"\s*]/g),
+ );
+ const referenced = matches.map(match => (match && match[1] ? match[1] : ''));
+ const hasTotalPrizes = referenced.some((ref) => {
+ const lower = ref.toLowerCase();
+ return (
+ lower.includes('total')
+ || lower.includes('prize')
+ || lower.includes('payment')
+ || lower.includes('amount')
+ );
+ });
+ if (hasTotalPrizes) {
+ const key = referenced.find(ref => ref) || 'total';
+ return {
+ path: '/statistics/general/total-prizes',
+ transform: data => ([{ [key]: data.total }]),
+ };
+ }
+ } catch (err) {
+ // swallow and fall through to unsupported handling
+ }
+ }
+
+ return null;
+}
+
+export default function SmartLooker(props) {
+ const { lookerId, property, render } = props;
+ const directPath = LOOKER_TO_REPORTS_PATH[lookerId];
+ const inferred = React.useMemo(
+ () => (directPath ? null : inferFromProps(property, render)),
+ [directPath, property, render],
+ );
+ const reportsPath = directPath || (inferred && inferred.path);
+ const transformer = inferred && inferred.transform;
+
+ const [state, setState] = React.useState({
+ loading: !!reportsPath,
+ error: null,
+ lookerInfo: null,
+ });
+
+ React.useEffect(() => {
+ let cancelled = false;
+
+ async function load() {
+ if (!reportsPath) return;
+ setState(s => ({ ...s, loading: true, error: null }));
+
+ try {
+ const res = await fetch(`${config.API.V6}/reports${reportsPath}`);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ let lookerData = Array.isArray(data) ? data : [data];
+ if (typeof transformer === 'function') {
+ const transformed = transformer(data);
+ if (Array.isArray(transformed)) lookerData = transformed;
+ else if (transformed !== undefined && transformed !== null) {
+ lookerData = [transformed];
+ } else {
+ lookerData = [];
+ }
+ }
+ if (!cancelled) {
+ setState({
+ loading: false,
+ error: null,
+ lookerInfo: { lookerData },
+ });
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setState({
+ loading: false,
+ error: err.message,
+ lookerInfo: { lookerData: [] },
+ });
+ }
+ }
+ }
+
+ load();
+ return () => {
+ cancelled = true;
+ };
+ }, [lookerId, reportsPath, transformer]);
+
+ if (!reportsPath) {
+ if (process.env.NODE_ENV !== 'production') {
+ // eslint-disable-next-line no-console
+ console.error(`SmartLooker: no reports mapping for Looker ID ${lookerId}`);
+ }
+ return 'Statistics report mapping missing.';
+ }
+
+ if (state.loading && !state.lookerInfo) return 'loading...';
+ if (state.error) return state.error;
+
+ return ;
+}
+
+SmartLooker.propTypes = {
+ lookerId: PT.string.isRequired,
+ property: PT.string,
+ render: PT.func,
+};
+
+SmartLooker.defaultProps = {
+ property: undefined,
+ render: undefined,
+};
diff --git a/src/shared/containers/SubmissionManagement/index.jsx b/src/shared/containers/SubmissionManagement/index.jsx
index a6e962c60e..0b0676b186 100644
--- a/src/shared/containers/SubmissionManagement/index.jsx
+++ b/src/shared/containers/SubmissionManagement/index.jsx
@@ -16,7 +16,7 @@ import { connect } from 'react-redux';
import { Modal, PrimaryButton } from 'topcoder-react-ui-kit';
import { config } from 'topcoder-react-utils';
import { actions, services } from 'topcoder-react-lib';
-import getReviewTypes from 'services/reviewTypes';
+import getReviewSummationsService from 'services/reviewSummations';
import { getSubmissionArtifacts, downloadSubmissions } from 'services/submissions';
import style from './styles.scss';
@@ -24,7 +24,121 @@ import smpActions from '../../actions/page/submission_management';
const { getService } = services.submissions;
-const { getService: getMemberService } = services.members;
+const SUMMATION_TYPE_PRIORITY = {
+ example: 0,
+ provisional: 1,
+ final: 2,
+ other: 3,
+};
+
+const normalizeTimestamp = (value) => {
+ if (!value) {
+ return 0;
+ }
+ const parsed = new Date(value).getTime();
+ return Number.isFinite(parsed) ? parsed : 0;
+};
+
+const ensureMetadataObject = metadata => ((metadata && typeof metadata === 'object') ? metadata : {});
+
+const deriveSummationType = (summation) => {
+ const metadata = ensureMetadataObject(_.get(summation, 'metadata'));
+ const metaStage = _.toLower(_.get(metadata, 'stage', ''));
+ const metaTestType = _.toLower(_.get(metadata, 'testType', ''));
+ const isExample = summation.isExample === true || metaTestType === 'example';
+ const isProvisional = summation.isProvisional === true || metaTestType === 'provisional';
+ const isFinal = summation.isFinal === true || metaStage === 'final';
+
+ if (isExample) {
+ return { key: 'example', label: 'Example' };
+ }
+ if (isProvisional) {
+ return { key: 'provisional', label: 'Provisional' };
+ }
+ if (isFinal) {
+ return { key: 'final', label: 'Final' };
+ }
+ return { key: 'other', label: 'Score' };
+};
+
+const mergeSubmissionWithSummations = (submission, reviewSummationsBySubmission) => {
+ if (!submission) {
+ return submission;
+ }
+ const submissionId = _.toString(submission.id || submission.submissionId);
+ if (!submissionId) {
+ return submission;
+ }
+
+ const summations = reviewSummationsBySubmission[submissionId];
+ if (!Array.isArray(summations) || !summations.length) {
+ return submission;
+ }
+
+ return {
+ ...submission,
+ reviewSummations: summations,
+ reviewSummation: summations,
+ };
+};
+
+const buildScoreEntries = (summations = []) => {
+ if (!Array.isArray(summations) || !summations.length) {
+ return [];
+ }
+
+ const latestByType = new Map();
+
+ summations.forEach((summation, index) => {
+ if (!summation) {
+ return;
+ }
+
+ const { key, label } = deriveSummationType(summation);
+ const timestampRaw = summation.reviewedDate
+ || summation.updatedAt
+ || summation.createdAt
+ || null;
+ const timestampValue = normalizeTimestamp(timestampRaw);
+ const reviewer = summation.updatedBy || summation.createdBy || 'System';
+ const aggregateScore = _.get(summation, 'aggregateScore');
+ let normalizedScore = null;
+ if (!_.isNil(aggregateScore) && aggregateScore !== '') {
+ if (Number.isFinite(aggregateScore)) {
+ normalizedScore = aggregateScore;
+ } else {
+ const parsedScore = Number(aggregateScore);
+ normalizedScore = Number.isFinite(parsedScore) ? parsedScore : aggregateScore;
+ }
+ }
+
+ const entry = {
+ id: summation.id || `${summation.submissionId || 'submission'}-${key}-${index}`,
+ label,
+ reviewer: reviewer || 'System',
+ score: _.isNil(normalizedScore) ? null : normalizedScore,
+ isPassing: typeof summation.isPassing === 'boolean' ? summation.isPassing : null,
+ reviewedOn: timestampRaw,
+ orderKey: key,
+ orderValue: SUMMATION_TYPE_PRIORITY[key] || SUMMATION_TYPE_PRIORITY.other,
+ timestampValue,
+ };
+
+ const existing = latestByType.get(key);
+ if (!existing || entry.timestampValue > existing.timestampValue) {
+ latestByType.set(key, entry);
+ }
+ });
+
+ return Array.from(latestByType.values())
+ .sort((a, b) => {
+ if (a.orderValue !== b.orderValue) {
+ return a.orderValue - b.orderValue;
+ }
+ return b.timestampValue - a.timestampValue;
+ })
+ .map(entry => _.omit(entry, ['orderKey', 'orderValue', 'timestampValue']));
+};
const theme = {
container: style.modalContainer,
@@ -35,14 +149,19 @@ class SubmissionManagementPageContainer extends React.Component {
constructor(props) {
super(props);
+ this.pendingReviewSummationChallengeId = null;
+ this.isComponentMounted = false;
+
this.state = {
needReload: false,
initialState: true,
submissions: [],
+ reviewSummationsBySubmission: {},
};
}
componentDidMount() {
+ this.isComponentMounted = true;
const {
authTokens,
challenge,
@@ -60,6 +179,8 @@ class SubmissionManagementPageContainer extends React.Component {
if (challengeId !== loadingSubmissionsForChallengeId) {
loadMySubmissions(authTokens, challengeId);
}
+
+ this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId);
}
componentWillReceiveProps(nextProps) {
@@ -73,6 +194,7 @@ class SubmissionManagementPageContainer extends React.Component {
this.setState({ needReload: true });
setTimeout(() => {
loadMySubmissions(authTokens, challengeId);
+ this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId);
this.setState({ needReload: false });
}, 2000);
}
@@ -84,17 +206,32 @@ class SubmissionManagementPageContainer extends React.Component {
deletionSucceed,
toBeDeletedId,
mySubmissions,
+ authTokens,
+ challengeId,
} = this.props;
const { initialState } = this.state;
if (initialState && mySubmissions) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
- submissions: [...mySubmissions],
+ submissions: this.buildSubmissionsArray(mySubmissions),
initialState: false,
});
return;
}
+
+ if (challengeId !== prevProps.challengeId
+ || _.get(authTokens, 'tokenV3') !== _.get(prevProps.authTokens, 'tokenV3')) {
+ this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId);
+ }
+
+ if (mySubmissions !== prevProps.mySubmissions && !initialState && mySubmissions) {
+ // eslint-disable-next-line react/no-did-update-set-state
+ this.setState({
+ submissions: this.buildSubmissionsArray(mySubmissions),
+ });
+ }
+
const { submissions } = this.state;
if (deletionSucceed !== prevProps.deletionSucceed) {
@@ -108,6 +245,101 @@ class SubmissionManagementPageContainer extends React.Component {
}
}
+ componentWillUnmount() {
+ this.isComponentMounted = false;
+ this.pendingReviewSummationChallengeId = null;
+ }
+
+ buildSubmissionsArray = (source) => {
+ const { reviewSummationsBySubmission } = this.state;
+ const base = Array.isArray(source) ? source : [];
+ return base.map(submission => (
+ mergeSubmissionWithSummations(submission, reviewSummationsBySubmission)
+ ));
+ };
+
+ refreshSubmissionScores = () => {
+ if (!this.isComponentMounted) {
+ return;
+ }
+ this.setState((prevState) => {
+ if (!Array.isArray(prevState.submissions) || !prevState.submissions.length) {
+ return null;
+ }
+ const updated = prevState.submissions.map(submission => (
+ mergeSubmissionWithSummations(submission, prevState.reviewSummationsBySubmission)
+ ));
+ if (_.isEqual(updated, prevState.submissions)) {
+ return null;
+ }
+ return { submissions: updated };
+ });
+ };
+
+ loadReviewSummations = async (tokenV3, challengeId) => {
+ const challengeIdStr = _.toString(challengeId);
+ if (!this.isComponentMounted || !tokenV3 || !challengeIdStr) {
+ if (this.isComponentMounted) {
+ this.setState({ reviewSummationsBySubmission: {} }, () => {
+ this.refreshSubmissionScores();
+ });
+ }
+ return;
+ }
+
+ this.pendingReviewSummationChallengeId = challengeIdStr;
+
+ try {
+ const { data } = await getReviewSummationsService(tokenV3, challengeIdStr);
+ if (!this.isComponentMounted || this.pendingReviewSummationChallengeId !== challengeIdStr) {
+ return;
+ }
+
+ const grouped = {};
+ (Array.isArray(data) ? data : []).forEach((summation) => {
+ if (!summation) {
+ return;
+ }
+ const submissionId = _.toString(_.get(summation, 'submissionId') || _.get(summation, 'id'));
+ if (!submissionId) {
+ return;
+ }
+ if (!grouped[submissionId]) {
+ grouped[submissionId] = [];
+ }
+ grouped[submissionId].push(summation);
+ });
+
+ Object.keys(grouped).forEach((key) => {
+ grouped[key].sort((a, b) => (
+ normalizeTimestamp(_.get(b, 'reviewedDate') || _.get(b, 'updatedAt') || _.get(b, 'createdAt'))
+ - normalizeTimestamp(_.get(a, 'reviewedDate') || _.get(a, 'updatedAt') || _.get(a, 'createdAt'))
+ ));
+ });
+
+ this.setState({ reviewSummationsBySubmission: grouped }, () => {
+ this.refreshSubmissionScores();
+ });
+ } catch (error) {
+ if (!this.isComponentMounted || this.pendingReviewSummationChallengeId !== challengeIdStr) {
+ return;
+ }
+ this.setState({ reviewSummationsBySubmission: {} }, () => {
+ this.refreshSubmissionScores();
+ });
+ }
+ };
+
+ getSubmissionScores = async (submissionId) => {
+ const submissionKey = _.toString(submissionId);
+ if (!submissionKey) {
+ return [];
+ }
+ const { reviewSummationsBySubmission } = this.state;
+ const summations = reviewSummationsBySubmission[submissionKey] || [];
+ return buildScoreEntries(summations);
+ };
+
render() {
const {
authTokens,
@@ -175,18 +407,7 @@ class SubmissionManagementPageContainer extends React.Component {
},
getSubmissionArtifacts:
submissionId => getSubmissionArtifacts(authTokens.tokenV3, submissionId),
- getReviewTypesList: () => {
- const reviewTypes = getReviewTypes(authTokens.tokenV3);
- return reviewTypes;
- },
- getChallengeResources: (cId) => {
- const membersService = getMemberService(authTokens.tokenV3);
- return membersService.getChallengeResources(cId);
- },
- getSubmissionInformation: (submissionId) => {
- const submissionsService = getService(authTokens.tokenV3);
- return submissionsService.getSubmissionInformation(submissionId);
- },
+ getSubmissionScores: submissionId => this.getSubmissionScores(submissionId),
onlineReviewUrl: `${config.URL.ONLINE_REVIEW}/review/actions/ViewProjectDetails?pid=${challengeId}`,
challengeUrl: `${challengesUrl}/${challengeId}`,
addSumissionUrl: `${config.URL.BASE}/challenges/${challengeId}/submit`,
diff --git a/src/shared/containers/SubmissionPage.jsx b/src/shared/containers/SubmissionPage.jsx
index 80579b39ab..88be1deed9 100644
--- a/src/shared/containers/SubmissionPage.jsx
+++ b/src/shared/containers/SubmissionPage.jsx
@@ -188,7 +188,7 @@ const mapStateToProps = (state, ownProps) => {
challengesUrl: ownProps.challengesUrl,
tokenV2: state.auth.tokenV2,
tokenV3: state.auth.tokenV3,
- track: details.track,
+ track: (details && details.track && details.track.name) ? details.track.name : details.track,
challenge: state.challenge,
status: details.status,
isRegistered: details.isRegistered,
diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx
index 272aebc19e..8ff11dc0ae 100644
--- a/src/shared/containers/challenge-detail/index.jsx
+++ b/src/shared/containers/challenge-detail/index.jsx
@@ -8,7 +8,12 @@
import _ from 'lodash';
import communityActions from 'actions/tc-communities';
-import { isMM as checkIsMM, isRDM as checkIsRDM } from 'utils/challenge';
+import {
+ isMM as checkIsMM,
+ isRDM as checkIsRDM,
+ getTrackName,
+ getTypeName,
+} from 'utils/challenge';
import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder';
import pageActions from 'actions/page';
import ChallengeHeader from 'components/challenge-detail/Header';
@@ -43,11 +48,15 @@ import {
SUBTRACKS,
CHALLENGE_STATUS,
} from 'utils/tc';
+import { hasOpenSubmissionPhase } from 'utils/challengePhases';
import { config } from 'topcoder-react-utils';
import MetaTags from 'components/MetaTags';
-import { actions } from 'topcoder-react-lib';
+import { decodeToken } from '@topcoder-platform/tc-auth-lib';
+import { actions, services } from 'topcoder-react-lib';
import { getService } from 'services/contentful';
import { getSubmissionArtifacts as getSubmissionArtifactsService } from 'services/submissions';
+import getReviewSummationsService from 'services/reviewSummations';
+import { buildMmSubmissionData, buildStatisticsData } from 'utils/mm-review-summations';
// import {
// getDisplayRecommendedChallenges,
// getRecommendedTags,
@@ -87,6 +96,7 @@ import './styles.scss';
/* Holds various time ranges in milliseconds. */
const MIN = 60 * 1000;
const DAY = 24 * 60 * MIN;
+const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
/**
* Given challenge details object, it returns the URL of the image to be used in
@@ -97,6 +107,8 @@ const DAY = 24 * 60 * MIN;
function getOgImage(challenge) {
const { legacy } = challenge;
const { subTrack } = legacy;
+ const trackName = getTrackName(challenge);
+ const typeName = getTypeName(challenge);
if (challenge.name.startsWith('LUX -')) return ogBigPrizesChallenge;
if (challenge.name.startsWith('RUX -')) return ogBigPrizesChallenge;
if (challenge.prizes) {
@@ -106,15 +118,15 @@ function getOgImage(challenge) {
switch (subTrack) {
case SUBTRACKS.FIRST_2_FINISH:
- switch (challenge.track) {
- case COMPETITION_TRACKS_V3.DEVELOP: return challenge.type === 'Task' ? ogDEVTask : ogFirst2FinishDEV;
- case COMPETITION_TRACKS_V3.QA: return challenge.type === 'Task' ? ogQATask : ogFirst2FinishQA;
+ switch (trackName) {
+ case COMPETITION_TRACKS_V3.DEVELOP: return typeName === 'Task' ? ogDEVTask : ogFirst2FinishDEV;
+ case COMPETITION_TRACKS_V3.QA: return typeName === 'Task' ? ogQATask : ogFirst2FinishQA;
default: return ogFirst2FinishDEV;
}
case SUBTRACKS.DESIGN_FIRST_2_FINISH:
- switch (challenge.track) {
- case COMPETITION_TRACKS_V3.DESIGN: return challenge.type === 'Task' ? ogDESIGNTask : ogFirst2FinishDESIGN;
+ switch (trackName) {
+ case COMPETITION_TRACKS_V3.DESIGN: return typeName === 'Task' ? ogDESIGNTask : ogFirst2FinishDESIGN;
default: return ogUiDesign;
}
@@ -139,11 +151,11 @@ function getOgImage(challenge) {
default:
}
- switch (challenge.track) {
+ switch (trackName) {
case COMPETITION_TRACKS_V3.DEVELOP: return ogDevelopment;
- case COMPETITION_TRACKS_V3.DESIGN: return challenge.type === 'Task' ? ogDESIGNTask : ogUiDesign;
+ case COMPETITION_TRACKS_V3.DESIGN: return typeName === 'Task' ? ogDESIGNTask : ogUiDesign;
case COMPETITION_TRACKS_V3.DS: return ogDSChallenge;
- case COMPETITION_TRACKS_V3.QA: return challenge.type === 'Task' ? ogQATask : ogQAChallenge;
+ case COMPETITION_TRACKS_V3.QA: return typeName === 'Task' ? ogQATask : ogQAChallenge;
default: return ogImage;
}
}
@@ -223,7 +235,7 @@ class ChallengeDetailPageContainer extends React.Component {
loadChallengeDetails(auth, challengeId);
}
- fetchChallengeStatistics(auth, challengeId);
+ fetchChallengeStatistics(auth, challenge);
if (!allCountries.length) {
getAllCountries(auth.tokenV3);
@@ -254,12 +266,21 @@ class ChallengeDetailPageContainer extends React.Component {
onSelectorClicked,
} = this.props;
+ const previousChallengeId = _.get(challenge, 'id');
+ const nextChallengeId = _.get(nextProps.challenge, 'id');
+
+ if (nextChallengeId
+ && nextChallengeId !== previousChallengeId
+ && checkIsMM(nextProps.challenge)) {
+ nextProps.fetchChallengeStatistics(nextProps.auth, nextProps.challenge);
+ }
+
if (challenge.isLegacyChallenge && !history.location.pathname.includes(challenge.id)) {
history.location.pathname = `/challenges/${challenge.id}`; // eslint-disable-line no-param-reassign
history.push(history.location.pathname, history.state);
}
- if (!checkIsMM(challenge) && COMPETITION_TRACKS_V3.DS !== challenge.track
+ if (!checkIsMM(challenge) && COMPETITION_TRACKS_V3.DS !== getTrackName(challenge)
&& selectedTab === DETAIL_TABS.MM_DASHBOARD) {
onSelectorClicked(DETAIL_TABS.DETAILS);
}
@@ -405,25 +426,28 @@ class ChallengeDetailPageContainer extends React.Component {
const {
legacy,
- legacyId,
status,
phases,
metadata,
} = challenge;
let { track } = legacy || {};
-
if (!track) {
/* eslint-disable prefer-destructuring */
track = challenge.track || '';
}
+ // Normalize track to a string if needed
+ track = getTrackName(track);
const submissionsViewable = _.find(metadata, { type: 'submissionsViewable' });
const isLoggedIn = !_.isEmpty(auth.tokenV3);
const { prizeSets } = challenge;
let challengePrizes = [];
- const placementPrizes = _.find(prizeSets, { type: 'placement' });
+ const placementPrizes = _.find(
+ prizeSets,
+ prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement',
+ );
if (placementPrizes) {
challengePrizes = _.filter(placementPrizes.prizes, p => p.value > 0);
}
@@ -457,7 +481,7 @@ class ChallengeDetailPageContainer extends React.Component {
}
let winners = challenge.winners || [];
- if (challenge.type !== 'Task') {
+ if (getTypeName(challenge) !== 'Task') {
winners = winners.filter(w => !w.type || w.type === 'final');
}
@@ -468,8 +492,7 @@ class ChallengeDetailPageContainer extends React.Component {
}
const submissionEnded = status === CHALLENGE_STATUS.COMPLETED
- || (!_.some(phases, { name: 'Submission', isOpen: true })
- && !_.some(phases, { name: 'Checkpoint Submission', isOpen: true }));
+ || !hasOpenSubmissionPhase(phases);
return (
@@ -682,17 +705,17 @@ class ChallengeDetailPageContainer extends React.Component {
)
}
- {legacyId && (
- {
- registerForChallenge(auth, challengeId);
- }}
- />
- )}
+
+ {
+ registerForChallenge(auth, challengeId);
+ }}
+ />
+
{showSecurityReminder && (
this.setState({ showSecurityReminder: false })}
@@ -747,13 +770,13 @@ ChallengeDetailPageContainer.defaultProps = {
allCountries: [],
reviewTypes: [],
isMenuOpened: false,
- loadingMMSubmissionsForChallengeId: '',
+ loadingMMSubmissionsForChallengeId: null,
mmSubmissions: [],
mySubmissions: [],
isLoadingSubmissionInformation: false,
submissionInformation: null,
// prizeMode: 'money-usd',
- statisticsData: null,
+ statisticsData: [],
getSubmissionArtifacts: () => {},
};
@@ -805,7 +828,10 @@ ChallengeDetailPageContainer.propTypes = {
unregistering: PT.bool.isRequired,
updateChallenge: PT.func.isRequired,
isMenuOpened: PT.bool,
- loadingMMSubmissionsForChallengeId: PT.string,
+ loadingMMSubmissionsForChallengeId: PT.oneOfType([
+ PT.string,
+ PT.oneOf([null]),
+ ]),
mmSubmissions: PT.arrayOf(PT.shape()),
loadMMSubmissions: PT.func.isRequired,
isLoadingSubmissionInformation: PT.bool,
@@ -837,46 +863,213 @@ function mapStateToProps(state, props) {
}));
if (challenge.submissions) {
- challenge.submissions = challenge.submissions.map(submission => ({
- ...submission,
- registrant: _.find(challenge.registrants, r => (`${r.memberId}` === `${submission.memberId}`)),
- }));
+ // Normalize submissions shape: API may return { data, meta } now.
+ const normalizedSubmissions = Array.isArray(challenge.submissions)
+ ? challenge.submissions
+ : (_.get(challenge, 'submissions.data') || []);
+
+ challenge.submissions = normalizedSubmissions.map((submission) => {
+ const registrant = _.find(
+ challenge.registrants,
+ r => (`${r.memberId}` === `${submission.memberId}`),
+ );
+ // Ensure legacy fields used in UI exist
+ const created = submission.created || submission.createdAt || null;
+ const updated = submission.updated || submission.updatedAt || null;
+ return ({
+ ...submission,
+ created,
+ updated,
+ registrant,
+ });
+ });
}
+ const loggedInUserId = _.get(auth, 'user.userId');
+ const loggedInUserHandle = _.get(auth, 'user.handle');
if (!_.isEmpty(mmSubmissions)) {
mmSubmissions = mmSubmissions.map((submission) => {
- let registrant;
- const { memberId } = submission;
- let member = memberId;
- if (`${auth.user.userId}` === `${memberId}`) {
- mySubmissions = submission.submissions || [];
- mySubmissions.forEach((mySubmission, index) => {
- mySubmissions[index].id = mySubmissions.length - index;
- });
+ let memberId = submission.memberId || submission.submitterId;
+ let registrant = submission.registrant;
+ let memberHandle = submission.member || submission.submitterHandle || '';
+ let ratingValue = null;
+ if (_.isFinite(submission.rating)) {
+ ratingValue = submission.rating;
+ } else if (_.isFinite(submission.submitterMaxRating)) {
+ ratingValue = submission.submitterMaxRating;
+ }
+
+ const normalizedAttempts = Array.isArray(submission.submissions)
+ ? submission.submissions.map((attempt, attemptIndex) => {
+ const normalizedAttempt = { ...attempt };
+ if (!normalizedAttempt.submissionTime) {
+ normalizedAttempt.submissionTime = normalizedAttempt.createdAt
+ || normalizedAttempt.created
+ || normalizedAttempt.reviewedDate
+ || normalizedAttempt.updatedAt
+ || null;
+ }
+ if (!normalizedAttempt.submissionId && normalizedAttempt.id) {
+ normalizedAttempt.submissionId = `${normalizedAttempt.id}`;
+ }
+
+ const attemptSummations = [];
+ if (Array.isArray(normalizedAttempt.reviewSummations)) {
+ attemptSummations.push(...normalizedAttempt.reviewSummations);
+ }
+ if (Array.isArray(normalizedAttempt.reviewSummation)) {
+ attemptSummations.push(...normalizedAttempt.reviewSummation);
+ }
+
+ let effectiveSummations = attemptSummations;
+ if (!effectiveSummations.length) {
+ const submissionLevelSummations = [];
+ if (Array.isArray(submission.reviewSummations)) {
+ submissionLevelSummations.push(...submission.reviewSummations);
+ }
+ if (Array.isArray(submission.reviewSummation)) {
+ submissionLevelSummations.push(...submission.reviewSummation);
+ }
+ // Fall back to submission-level scores for the latest attempt when
+ // per-attempt review summaries are not available.
+ effectiveSummations = submissionLevelSummations.length && attemptIndex === 0
+ ? submissionLevelSummations
+ : attemptSummations;
+ }
+
+ const toNumericScore = (value) => {
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+ };
+
+ const findScore = (summations, predicate) => {
+ if (!Array.isArray(summations) || !summations.length) {
+ return null;
+ }
+ const match = _.find(summations, predicate);
+ if (!match) {
+ return null;
+ }
+ return toNumericScore(match.aggregateScore);
+ };
+
+ const hasProvisionalScore = !_.isNil(normalizedAttempt.provisionalScore);
+ const hasFinalScore = !_.isNil(normalizedAttempt.finalScore);
+
+ if (!hasProvisionalScore) {
+ const provisionalScore = findScore(
+ effectiveSummations,
+ s => s && (s.isProvisional || _.get(s, 'type', '').toLowerCase() === 'provisional'),
+ );
+ if (!_.isNil(provisionalScore)) {
+ normalizedAttempt.provisionalScore = provisionalScore;
+ }
+ }
+
+ if (!hasFinalScore) {
+ const finalScore = findScore(
+ effectiveSummations,
+ s => s && (s.isFinal || _.get(s, 'type', '').toLowerCase() === 'final'),
+ );
+ if (!_.isNil(finalScore)) {
+ normalizedAttempt.finalScore = finalScore;
+ }
+ }
+ if (process.env.NODE_ENV !== 'production'
+ && _.isNil(normalizedAttempt.provisionalScore)
+ && _.isNil(normalizedAttempt.finalScore)
+ && effectiveSummations.length) {
+ // eslint-disable-next-line no-console
+ console.warn('Submission attempt missing review scores despite summations', {
+ attemptId: normalizedAttempt.submissionId || normalizedAttempt.id,
+ });
+ }
+
+ return normalizedAttempt;
+ })
+ : [];
+
+ const normalizedHandle = _.toLower(memberHandle || '');
+ let submissionDetail = null;
+ if (memberId) {
+ submissionDetail = _.find(
+ challenge.submissions,
+ s => (`${s.memberId}` === `${memberId}`),
+ );
+ }
+ if (!submissionDetail && normalizedHandle) {
+ submissionDetail = _.find(
+ challenge.submissions,
+ (s) => {
+ const submissionHandle = _.toLower(_.get(s, 'registrant.memberHandle')
+ || _.get(s, 'memberHandle')
+ || _.get(s, 'createdBy')
+ || '');
+ return submissionHandle && submissionHandle === normalizedHandle;
+ },
+ );
}
- const submissionDetail = _.find(challenge.submissions, s => (`${s.memberId}` === `${submission.memberId}`));
if (submissionDetail) {
- member = submissionDetail.createdBy;
- ({ registrant } = submissionDetail);
+ registrant = registrant || submissionDetail.registrant;
+ memberHandle = memberHandle || submissionDetail.createdBy;
+ if (!memberId && submissionDetail.memberId) {
+ memberId = `${submissionDetail.memberId}`;
+ }
}
- if (!registrant) {
+ if (!registrant && memberId) {
registrant = _.find(challenge.registrants, r => `${r.memberId}` === `${memberId}`);
}
+ if (!registrant && normalizedHandle) {
+ registrant = _.find(
+ challenge.registrants,
+ (r) => {
+ const registrantHandle = _.toLower(r.memberHandle || r.handle || '');
+ return registrantHandle && registrantHandle === normalizedHandle;
+ },
+ );
+ }
if (registrant) {
- member = registrant.memberHandle;
+ memberHandle = memberHandle || registrant.memberHandle;
+ if (!_.isFinite(ratingValue) && _.isFinite(registrant.rating)) {
+ ratingValue = registrant.rating;
+ }
+ if (!memberId && registrant.memberId) {
+ memberId = `${registrant.memberId}`;
+ }
+ }
+
+ if (!memberHandle && !_.isNil(memberId)) {
+ memberHandle = `${memberId}`;
+ }
+
+ const isLoggedInSubmitter = (
+ memberId && loggedInUserId && `${loggedInUserId}` === `${memberId}`
+ ) || (
+ normalizedHandle
+ && loggedInUserHandle
+ && normalizedHandle === _.toLower(loggedInUserHandle)
+ );
+
+ if (isLoggedInSubmitter) {
+ mySubmissions = normalizedAttempts.map((attempt, index) => ({
+ ...attempt,
+ id: normalizedAttempts.length - index,
+ }));
}
return ({
...submission,
+ submissions: normalizedAttempts,
registrant,
- member,
+ member: memberHandle,
+ rating: ratingValue,
});
});
- } else {
- mySubmissions = _.filter(challenge.submissions, s => (`${s.memberId}` === `${auth.user.userId}`));
+ } else if (loggedInUserId) {
+ mySubmissions = _.filter(challenge.submissions, s => (`${s.memberId}` === `${loggedInUserId}`));
}
}
const { page: { challengeDetails: { feedbackOpen } } } = state;
@@ -938,6 +1131,76 @@ function mapStateToProps(state, props) {
const mapDispatchToProps = (dispatch) => {
const ca = communityActions.tcCommunity;
const lookupActions = actions.lookup;
+ const challengeActions = actions.challenge || {};
+ const hasReviewSummationsActions = (
+ typeof challengeActions.getReviewSummationsInit === 'function'
+ && typeof challengeActions.getReviewSummationsDone === 'function'
+ );
+
+ const dispatchReviewSummations = (challengeId, tokenV3) => {
+ const challengeIdStr = _.toString(challengeId);
+ if (!challengeIdStr) {
+ return;
+ }
+
+ if (hasReviewSummationsActions) {
+ dispatch(challengeActions.getReviewSummationsInit(challengeIdStr));
+ dispatch(challengeActions.getReviewSummationsDone(challengeIdStr, tokenV3));
+ return;
+ }
+
+ dispatch({
+ type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_INIT',
+ payload: challengeIdStr,
+ });
+ dispatch({
+ type: 'CHALLENGE/GET_MM_SUBMISSIONS_INIT',
+ payload: challengeIdStr,
+ });
+
+ getReviewSummationsService(tokenV3, challengeIdStr)
+ .then(({ data }) => {
+ const reviewSummations = Array.isArray(data) ? data : [];
+ const mmSubmissions = buildMmSubmissionData(reviewSummations);
+ const statisticsData = buildStatisticsData(reviewSummations);
+
+ dispatch({
+ type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_DONE',
+ payload: reviewSummations,
+ meta: { challengeId: challengeIdStr },
+ });
+ dispatch({
+ type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE',
+ payload: {
+ challengeId: challengeIdStr,
+ submissions: mmSubmissions,
+ },
+ });
+ dispatch({
+ type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE',
+ payload: statisticsData,
+ });
+ })
+ .catch((error) => {
+ dispatch({
+ type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_DONE',
+ error: true,
+ payload: { challengeId: challengeIdStr, error },
+ meta: { challengeId: challengeIdStr },
+ });
+ dispatch({
+ type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE',
+ error: true,
+ payload: { challengeId: challengeIdStr, error },
+ });
+ dispatch({
+ type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE',
+ error: true,
+ payload: error,
+ });
+ });
+ };
+
return {
// getAllRecommendedChallenges: (tokenV3, recommendedTechnology) => {
// const uuid = shortId();
@@ -968,7 +1231,8 @@ const mapDispatchToProps = (dispatch) => {
dispatch(a.getDetailsDone(challengeId, tokens.tokenV3, tokens.tokenV2))
.then((res) => {
const ch = res.payload;
- if (ch.track === COMPETITION_TRACKS.DES) {
+ const chTrack = (ch && ch.track && ch.track.name) ? ch.track.name : ch.track;
+ if (chTrack === COMPETITION_TRACKS.DES) {
const p = ch.phases || []
.filter(x => x.name === 'Checkpoint Review');
if (p.length && !p[0].isOpen) {
@@ -986,13 +1250,66 @@ const mapDispatchToProps = (dispatch) => {
registerForChallenge: (auth, challengeId) => {
const a = actions.challenge;
dispatch(a.registerInit());
- dispatch(a.registerDone(auth, challengeId));
+
+ const challengeService = services.challenge.getService(auth.tokenV3, auth.tokenV2);
+ const apiV6 = services.api.getApi('V6', auth.tokenV3);
+ const actionType = a.registerDone.toString();
+
+ const payload = (async () => {
+ try {
+ if (!auth.tokenV3) {
+ throw new Error('Authentication token is required to register.');
+ }
+
+ const roleId = await challengeService.getRoleId('Submitter');
+ const user = decodeToken(auth.tokenV3);
+ const requestBody = {
+ challengeId,
+ memberHandle: encodeURIComponent(user.handle),
+ roleId,
+ };
+
+ const response = await apiV6.postJson('/resources', requestBody);
+ let responseData = null;
+
+ if (!response.ok) {
+ try {
+ responseData = await response.json();
+ } catch (parseError) {
+ responseData = null;
+ }
+ const error = new Error((responseData && responseData.message)
+ || response.statusText
+ || 'Failed to register for the challenge.');
+ error.status = response.status;
+ if (responseData && responseData.metadata && responseData.metadata.missingTerms) {
+ error.missingTerms = responseData.metadata.missingTerms;
+ }
+ throw error;
+ }
+
+ await response.json().catch(() => null);
+ await wait(config.CHALLENGE_DETAILS_REFRESH_DELAY);
+ return challengeService.getChallengeDetails(challengeId);
+ } catch (err) {
+ if (err.status === 403 && err.missingTerms) {
+ dispatch(termsActions.terms.openTermsModal('ANY'));
+ }
+ throw err;
+ }
+ })();
+
+ return dispatch({
+ type: actionType,
+ payload,
+ });
},
reloadChallengeDetails: (tokens, challengeId) => {
const a = actions.challenge;
dispatch(a.getDetailsDone(challengeId, tokens.tokenV3, tokens.tokenV2))
.then((challengeDetails) => {
- if (challengeDetails.track === COMPETITION_TRACKS.DES) {
+ const trackName = _.get(challengeDetails, 'track.name', challengeDetails.track);
+ if (trackName === COMPETITION_TRACKS.DES) {
const p = challengeDetails.phases || []
.filter(x => x.name === 'Checkpoint Review');
if (p.length && !p[0].isOpen) {
@@ -1053,9 +1370,7 @@ const mapDispatchToProps = (dispatch) => {
dispatch(a.updateChallengeDone(uuid, challenge, tokenV3));
},
loadMMSubmissions: (challengeId, tokenV3) => {
- const a = actions.challenge;
- dispatch(a.getMmSubmissionsInit(challengeId));
- dispatch(a.getMmSubmissionsDone(challengeId, tokenV3));
+ dispatchReviewSummations(challengeId, tokenV3);
},
getSubmissionArtifacts:
(submissionId, tokenV3) => getSubmissionArtifactsService(tokenV3, submissionId),
@@ -1068,10 +1383,17 @@ const mapDispatchToProps = (dispatch) => {
const a = challengeListingActions.challengeListing;
dispatch(a.expandTag(id));
},
- fetchChallengeStatistics: (tokens, challengeId) => {
- const a = actions.challenge;
- dispatch(a.fetchChallengeStatisticsInit());
- dispatch(a.fetchChallengeStatisticsDone(challengeId, tokens.tokenV3));
+ fetchChallengeStatistics: (tokens, challengeDetails) => {
+ if (!tokens || !tokens.tokenV3 || !challengeDetails || !checkIsMM(challengeDetails)) {
+ return;
+ }
+
+ const challengeId = _.toString(challengeDetails.id || challengeDetails.legacyId);
+ if (!challengeId) {
+ return;
+ }
+
+ dispatchReviewSummations(challengeId, tokens.tokenV3);
},
};
};
diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx
index 36a342925f..367e70f732 100644
--- a/src/shared/containers/challenge-listing/FilterPanel.jsx
+++ b/src/shared/containers/challenge-listing/FilterPanel.jsx
@@ -17,10 +17,10 @@ import { connect } from 'react-redux';
import qs from 'qs';
import _ from 'lodash';
import { createStaticRanges } from 'utils/challenge-listing/date-range';
+import { EXCLUDED_CHALLENGE_TYPE_NAMES } from 'utils/challenge-listing/constants';
const MIN = 60 * 1000;
-
export class Container extends React.Component {
constructor(props) {
super(props);
@@ -96,13 +96,26 @@ export class Container extends React.Component {
setFilterState,
validTypes,
} = this.props;
+ const currentTypes = filterState.types || [];
- if (!filterState.types.length && validTypes.length && !this.initialDefaultChallengeTypes) {
+ if (!currentTypes.length && validTypes.length && !this.initialDefaultChallengeTypes) {
setFilterState({
..._.clone(filterState),
types: validTypes.map(item => item.abbreviation),
});
this.initialDefaultChallengeTypes = true;
+ } else if (validTypes.length && currentTypes.length) {
+ const validAbbreviations = validTypes.map(item => item.abbreviation);
+ const sanitizedTypes = currentTypes.filter(type => validAbbreviations.includes(type));
+ if (sanitizedTypes.length !== currentTypes.length) {
+ if (!sanitizedTypes.length) {
+ this.initialDefaultChallengeTypes = false;
+ }
+ setFilterState({
+ ..._.clone(filterState),
+ types: sanitizedTypes,
+ });
+ }
}
}
@@ -222,6 +235,23 @@ function mapDispatchToProps(dispatch) {
function mapStateToProps(state, ownProps) {
const cl = state.challengeListing;
const tc = state.tcCommunities;
+ const filteredChallengeTypes = cl.challengeTypes
+ .filter(type => !EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name));
+ const excludedTypeAbbreviations = cl.challengeTypes
+ .filter(type => EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name))
+ .map(type => type.abbreviation);
+ let filterState = cl.filter;
+ const existingTypes = Array.isArray(cl.filter.types) ? cl.filter.types : [];
+ if (excludedTypeAbbreviations.length && existingTypes.length) {
+ const sanitizedTypes = existingTypes
+ .filter(type => !excludedTypeAbbreviations.includes(type));
+ if (sanitizedTypes.length !== existingTypes.length) {
+ filterState = {
+ ...cl.filter,
+ types: sanitizedTypes,
+ };
+ }
+ }
return {
...ownProps,
...state.challengeListing.filterPanel,
@@ -229,9 +259,9 @@ function mapStateToProps(state, ownProps) {
communityFilters: tc.list.data,
communityList: tc.list,
defaultCommunityId: ownProps.defaultCommunityId,
- filterState: cl.filter,
+ filterState,
loadingTypes: cl.loadingChallengeTypes,
- validTypes: cl.challengeTypes,
+ validTypes: filteredChallengeTypes,
selectedCommunityId: cl.selectedCommunityId,
auth: state.auth,
tokenV2: state.auth.tokenV2,
diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx
index a844a84917..91dad78378 100644
--- a/src/shared/containers/challenge-listing/Listing/index.jsx
+++ b/src/shared/containers/challenge-listing/Listing/index.jsx
@@ -602,7 +602,7 @@ export class ListingContainer extends React.Component {
let loadMoreReviewOpportunities;
if (!allReviewOpportunitiesLoaded) {
loadMoreReviewOpportunities = () => getReviewOpportunities(
- 1 + lastRequestedPageOfReviewOpportunities, tokenV3,
+ 1 + lastRequestedPageOfReviewOpportunities,
);
}
@@ -1001,10 +1001,10 @@ function mapDispatchToProps(dispatch) {
dispatch(a.getPastChallengesInit(uuid, page, frontFilter));
dispatch(a.getPastChallengesDone(uuid, page, filter, token, frontFilter));
},
- getReviewOpportunities: (page, token) => {
+ getReviewOpportunities: (page) => {
const uuid = shortId();
dispatch(a.getReviewOpportunitiesInit(uuid, page));
- dispatch(a.getReviewOpportunitiesDone(uuid, page, token));
+ dispatch(a.getReviewOpportunitiesDone(uuid, page));
},
getCopilotOpportunities: (page) => {
const uuid = shortId();
diff --git a/src/shared/containers/tc-communities/cognitive/home.jsx b/src/shared/containers/tc-communities/cognitive/home.jsx
index aa817e8b7a..53ebdc8222 100644
--- a/src/shared/containers/tc-communities/cognitive/home.jsx
+++ b/src/shared/containers/tc-communities/cognitive/home.jsx
@@ -54,7 +54,7 @@ class HomeContainer extends React.Component {
if (filter) {
filter = Filter.getFilterFunction(filter.challengeFilter);
challenges = activeChallenges
- .filter(x => x.status === 'Active')
+ .filter(x => x.status === 'ACTIVE')
.filter(filter)
.sort((a, b) => moment(a.registrationStartDate).diff(b.registrationStartDate));
}
diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js
index 2719e90409..a35a06d046 100644
--- a/src/shared/reducers/challenge-listing/index.js
+++ b/src/shared/reducers/challenge-listing/index.js
@@ -15,6 +15,7 @@ import {
actions as actionsUtils,
} from 'topcoder-react-lib';
import { REVIEW_OPPORTUNITY_TYPES } from 'utils/tc';
+import { EXCLUDED_CHALLENGE_TYPE_NAMES } from 'utils/challenge-listing/constants';
import filterPanel from './filter-panel';
import sidebar, { factory as sidebarFactory } from './sidebar';
@@ -405,15 +406,24 @@ function onSetFilter(state, { payload }) {
/* Validation of filter parameters: they may come from URL query, thus
* validation is not a bad idea. As you may note, at the moment we do not
* do it very carefuly (many params are not validated). */
+ const basePayload = _.isPlainObject(payload) ? payload : {};
+ const sanitizedPayload = { ...basePayload };
+ const excludedTypeAbbreviations = state.challengeTypes
+ .filter(type => EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name))
+ .map(type => type.abbreviation);
+ if (excludedTypeAbbreviations.length && Array.isArray(basePayload.types)) {
+ sanitizedPayload.types = basePayload.types
+ .filter(type => !excludedTypeAbbreviations.includes(type));
+ }
const filter = _.pickBy(_.pick(
- payload,
+ sanitizedPayload,
['tags', 'types', 'search', 'startDateEnd', 'endDateStart', 'groups', 'events', 'tracks', 'tco', 'isInnovationChallenge'],
), value => (!_.isArray(value) && value && value !== '') || (_.isArray(value) && value.length > 0));
const emptyArrayAllowedFields = ['types'];
emptyArrayAllowedFields.forEach((field) => {
- if (_.isEqual(payload[field], [])) {
- filter[field] = payload[field];
+ if (_.isEqual(sanitizedPayload[field], [])) {
+ filter[field] = sanitizedPayload[field];
}
});
@@ -437,7 +447,7 @@ function onSetFilter(state, { payload }) {
// console.log(`======`);
return {
...state,
- filter: _.assign({}, state.filter, payload),
+ filter: _.assign({}, state.filter, sanitizedPayload),
/* Page numbers of past/upcoming challenges depend on the filters. To keep
* the code simple we just reset them each time a filter is modified. */
@@ -482,9 +492,9 @@ function onGetReviewOpportunitiesDone(state, { payload, error }) {
if (uuid !== state.loadingReviewOpportunitiesUUID) return state;
const ids = new Set();
- loaded.forEach(item => ids.add(item.id));
+ loaded.forEach(item => ids.add(item.challengeId));
const reviewOpportunities = state.reviewOpportunities
- .filter(item => !ids.has(item.id))
+ .filter(item => !ids.has(item.challengeId))
.concat(loaded);
return {
diff --git a/src/shared/reducers/page/review-opportunity-details.js b/src/shared/reducers/page/review-opportunity-details.js
index 39b8127e44..fe9fa08a49 100644
--- a/src/shared/reducers/page/review-opportunity-details.js
+++ b/src/shared/reducers/page/review-opportunity-details.js
@@ -3,6 +3,52 @@ import { handleActions } from 'redux-actions';
import actions, { TABS } from 'actions/page/review-opportunity-details';
+/**
+ * Generates a list of unique terms ids required for the open review roles
+ * with an agreed field
+ *
+ * @param {Object} details Review Opportuny details from API
+ * @return {Array} List of unique terms
+ */
+function buildRequiredTermsList(details) {
+ const roles = details.payments.map(payment => payment.role);
+
+ const requiredTerms = _.uniqBy(
+ details.challenge.terms
+ // Sometimes roles such as Primary Reviewer have no directly equal
+ // terms entry. Include the plain Reviewer terms when present as a back-up.
+ .filter(term => term.role === 'Reviewer' || _.includes(roles, term.role))
+ .map(term => _.pick(term, ['id', 'agreed', 'title'])),
+ term => term.id,
+ );
+
+ return requiredTerms || [];
+}
+
+
+/**
+ * Handles REVIEW_OPPORTUNITY/GET__DETAILS_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetDetailsDone(state, { payload, error }) {
+ if (error) {
+ return {
+ ...state,
+ authError: true,
+ isLoadingDetails: false,
+ };
+ }
+
+ return {
+ ...state,
+ details: payload.details,
+ isLoadingDetails: false,
+ requiredTerms: buildRequiredTermsList(payload.details),
+ };
+}
+
/**
* Creates a new reducer.
* @param {Object} state Optional. Initial state.
@@ -11,6 +57,8 @@ import actions, { TABS } from 'actions/page/review-opportunity-details';
function create(defaultState = {}) {
const a = actions.page.reviewOpportunityDetails;
return handleActions({
+ [a.getDetailsInit]: state => ({ ...state, isLoadingDetails: true }),
+ [a.getDetailsDone]: onGetDetailsDone,
[a.selectTab]: (state, { payload }) => ({ ...state, selectedTab: payload }),
[a.setRoles]: (state, { payload }) => ({ ...state, selectedRoles: payload }),
[a.toggleApplyModal]: state => ({ ...state, applyModalOpened: !state.applyModalOpened }),
diff --git a/src/shared/routes/Communities/Veterans/Routes.jsx b/src/shared/routes/Communities/Veterans/Routes.jsx
index c15f932bc7..7ebf832deb 100644
--- a/src/shared/routes/Communities/Veterans/Routes.jsx
+++ b/src/shared/routes/Communities/Veterans/Routes.jsx
@@ -31,7 +31,7 @@ export default function Veterans({ base, member, meta }) {
registerBucket(ID, {
filter: {
...meta.challengeFilter,
- status: 'Active',
+ status: 'ACTIVE',
},
hideCount: false,
name: 'Active Veterans Challenges',
diff --git a/src/shared/services/dashboard.js b/src/shared/services/dashboard.js
index cbcd3cc60a..8fb874bc9d 100644
--- a/src/shared/services/dashboard.js
+++ b/src/shared/services/dashboard.js
@@ -5,12 +5,12 @@ const { getApi } = services.api;
class DashboardService {
/**
- * @param {String} tokenV5 Optional. Auth token for Topcoder API v5.
+ * @param {String} tokenV6 Optional. Auth token for Topcoder API v6.
*/
- constructor(tokenV5) {
+ constructor(tokenV6) {
this.private = {
- api: getApi('V5', tokenV5),
- tokenV5,
+ api: getApi('V6', tokenV6),
+ tokenV6,
};
}
@@ -27,13 +27,13 @@ class DashboardService {
/**
* Returns a new or existing challenges service.
- * @param {String} tokenV5 Optional. Auth token for Topcoder API v5.
+ * @param {String} tokenV6 Optional. Auth token for Topcoder API v6.
* @return {DashboardService} Dashboard service object
*/
let lastInstance = null;
-export function getService(tokenV5) {
- if (!lastInstance || tokenV5 !== lastInstance.private.tokenV5) {
- lastInstance = new DashboardService(tokenV5);
+export function getService(tokenV6) {
+ if (!lastInstance || tokenV6 !== lastInstance.private.tokenV6) {
+ lastInstance = new DashboardService(tokenV6);
}
return lastInstance;
}
diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js
new file mode 100644
index 0000000000..d9e525e654
--- /dev/null
+++ b/src/shared/services/reviewOpportunities.js
@@ -0,0 +1,90 @@
+import { config } from 'topcoder-react-utils';
+
+const v6ApiUrl = config.API.V6;
+
+/**
+ * Fetches copilot opportunities.
+ *
+ * @param {number} page - Page number (1-based).
+ * @param {number} pageSize - Number of items per page.
+ * @returns {Promise
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss index 1aa3016218..7c13c67016 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss +++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss @@ -1,6 +1,6 @@ @import "~styles/mixins"; -.comtainer { +.container { background: $tc-gray-neutral-dark; width: 100%; display: flex; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx index cc48ef7541..0013a94009 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx @@ -51,7 +51,7 @@ const ApplyTime = ({ disabled={!timeLeft || !openPositions} onClick={() => onApply()} > - {hasApplied ? 'Manage Applications' : 'Apply for review'} + {hasApplied ? 'View Application' : 'Apply for review'}
- {details.challenge.title} + {details.challenge.name}
Current Deadline Ends: {' '} @@ -184,7 +183,7 @@ export default function SubmissionManagement(props) { challenge={challenge} submissionObjects={submissions} showDetails={showDetails} - track={track} + track={trackName} status={challenge.status} submissionPhaseStartDate={submissionPhaseStartDate} {...componentConfig} @@ -192,7 +191,7 @@ export default function SubmissionManagement(props) { ) }
${currentPointer.customData.submissionCount} submissions
Score: ${this.y}
-Submitted: ${moment(currentPointer.customData.created).format('MM/DD/YYYY')}
+Submitted: ${moment(currentPointer.customData.created || currentPointer.customData.createdAt).format('MM/DD/YYYY')}
- {moment(s.created).format('MMM DD, YYYY HH:mm')} + {moment(s.created || s.createdAt).format('MMM DD, YYYY HH:mm')}
{ - (s.reviewSummation && s.reviewSummation[0].aggregateScore && challenge.status === 'Completed') - ? s.reviewSummation[0].aggregateScore.toFixed(2) + (s.review && s.finalScore && challenge.status === 'COMPLETED') + ? Number(s.finalScore).toFixed(2) : 'N/A' }
@@ -1012,7 +1044,10 @@ SubmissionsComponent.propTypes = { submissionHistoryOpen: PT.shape({}).isRequired, loadMMSubmissions: PT.func.isRequired, mmSubmissions: PT.arrayOf(PT.shape()).isRequired, - loadingMMSubmissionsForChallengeId: PT.string.isRequired, + loadingMMSubmissionsForChallengeId: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]).isRequired, isLoadingSubmissionInformation: PT.bool, submissionInformation: PT.shape(), loadSubmissionInformation: PT.func.isRequired, diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index 503718e85e..73189a5914 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -11,6 +11,7 @@ import { getTimeLeft, } from 'utils/challenge-detail/helper'; +import { getTypeName } from 'utils/challenge'; import ChallengeProgressBar from '../../ChallengeProgressBar'; import ProgressBarTooltip from '../../Tooltips/ProgressBarTooltip'; import UserAvatarTooltip from '../../Tooltips/UserAvatarTooltip'; @@ -232,14 +233,14 @@ export default function ChallengeStatus(props) { .filter(p => p.name !== 'Registration' && p.isOpen) .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0]; - if (!statusPhase && type === 'First2Finish' && allPhases.length) { + if (!statusPhase && getTypeName({ type }) === 'First2Finish' && allPhases.length) { statusPhase = _.clone(allPhases[0]); statusPhase.name = 'Submission'; } let phaseMessage = STALLED_MSG; if (statusPhase) phaseMessage = statusPhase.name; - else if (status === 'Draft') phaseMessage = DRAFT_MSG; + else if (status === 'DRAFT') phaseMessage = DRAFT_MSG; const showRegisterInfo = false; @@ -287,7 +288,7 @@ export default function ChallengeStatus(props) {- { start.isAfter() ? formatDuration(start.diff()) : ` ${formatDuration(-start.diff())}` } + {isLate ? 'Late by' : 'Time left'}
+ {isLate + ? formatDuration(now.diff(start)) + : formatDuration(start.diff(now)) + } to Apply diff --git a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx index d79b811a07..a33c1943a4 100644 --- a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx +++ b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx @@ -13,6 +13,7 @@ import { import { Link } from 'topcoder-react-utils'; import { COMPETITION_TRACKS } from 'utils/tc'; +import { getTrackName } from 'utils/challenge'; import './style.scss'; @@ -29,7 +30,7 @@ export default function Card({ } = challenge; let TrackTag; - switch (track.toLowerCase()) { + switch ((getTrackName(track) || '').toLowerCase()) { case 'datasci': case COMPETITION_TRACKS.DS: TrackTag = DataScienceTrackTag; diff --git a/src/shared/containers/Dashboard/ChallengesFeed.jsx b/src/shared/containers/Dashboard/ChallengesFeed.jsx index 113aa5881c..b8ede649ca 100644 --- a/src/shared/containers/Dashboard/ChallengesFeed.jsx +++ b/src/shared/containers/Dashboard/ChallengesFeed.jsx @@ -23,7 +23,7 @@ class ChallengesFeedContainer extends React.Component { perPage: excludeTags && excludeTags.length ? undefined : itemCount, types: ['CH', 'F2F', 'MM'], tracks, - status: 'Active', + status: 'ACTIVE', sortBy: 'updated', sortOrder: 'desc', isLightweight: true, diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index bb40d2f79e..3b6ae8b17c 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -10,11 +10,12 @@ import { connect } from 'react-redux'; import { actions, errors } from 'topcoder-react-lib'; import LoadingIndicator from 'components/LoadingIndicator'; -import { activeRoleIds } from 'utils/reviewOpportunities'; import pageActions from 'actions/page/review-opportunity-details'; import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage'; import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad'; import termsActions from 'actions/terms'; +import { goToLogin } from 'utils/tc'; +import { logger } from 'tc-core-library-js'; const { fireErrorMessage } = errors; @@ -25,6 +26,7 @@ class ReviewOpportunityDetailsContainer extends React.Component { componentDidMount() { const { challengeId, + opportunityId, details, isLoadingDetails, loadDetails, @@ -32,19 +34,37 @@ class ReviewOpportunityDetailsContainer extends React.Component { } = this.props; if (!isLoadingDetails && !details) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } else if (details.challenge.id !== challengeId) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } } handleOnHeaderApply() { const { + isLoggedIn, + isReviewer, openTermsModal, terms, termsFailure, toggleApplyModal, } = this.props; + + if (!isLoggedIn) { + goToLogin('community-app-main'); + return; + } + + if (!isReviewer) { + fireErrorMessage( + 'Permission Required', + + You must have a reviewer role to apply for this review opportunity. + , + ); + return; + } + if (termsFailure) { fireErrorMessage('Error Getting Terms Details', ''); return; @@ -56,45 +76,28 @@ class ReviewOpportunityDetailsContainer extends React.Component { } } - handleOnModalApply() { + async handleOnModalApply() { const { - cancelApplications, challengeId, - details, - handle, + opportunityId, loadDetails, - selectedRoles, submitApplications, toggleApplyModal, tokenV3, } = this.props; - const rolesToApply = []; - const rolesToCancel = []; - - const previousRoles = activeRoleIds(details, handle); + try { + // Wait for the submit to finish (and succeed) + await submitApplications(opportunityId, tokenV3); - previousRoles.forEach((id) => { - if (!_.includes(selectedRoles, id)) { - rolesToCancel.push(id); - } - }); - - selectedRoles.forEach((id) => { - if (!_.includes(previousRoles, id)) { - rolesToApply.push(id); - } - }); + toggleApplyModal(); - if (rolesToApply.length) { - submitApplications(challengeId, rolesToApply, tokenV3); - } - if (rolesToCancel.length) { - cancelApplications(challengeId, rolesToCancel, tokenV3); + await loadDetails(challengeId, opportunityId, tokenV3); + } catch (err) { + logger.error('submitApplications failed', err); + toggleApplyModal(); } - - toggleApplyModal(); - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } render() { @@ -130,6 +133,8 @@ ReviewOpportunityDetailsContainer.defaultProps = { termsFailure: false, phasesExpanded: false, tokenV3: null, + isLoggedIn: false, + isReviewer: false, }; /** @@ -140,6 +145,7 @@ ReviewOpportunityDetailsContainer.propTypes = { authError: PT.bool, cancelApplications: PT.func.isRequired, challengeId: PT.string.isRequired, + opportunityId: PT.string.isRequired, details: PT.shape(), handle: PT.string.isRequired, isLoadingDetails: PT.bool, @@ -157,6 +163,8 @@ ReviewOpportunityDetailsContainer.propTypes = { toggleRole: PT.func.isRequired, onPhaseExpand: PT.func.isRequired, tokenV3: PT.string, + isLoggedIn: PT.bool, + isReviewer: PT.bool, }; /** @@ -169,12 +177,14 @@ ReviewOpportunityDetailsContainer.propTypes = { const mapStateToProps = (state, ownProps) => { const api = state.reviewOpportunity; const page = state.page.reviewOpportunityDetails; + const queryParams = new URLSearchParams(ownProps.location.search); const { terms } = state; return { authError: api.authError, applyModalOpened: page.applyModalOpened, challengeId: String(ownProps.match.params.challengeId), - details: api.details, + opportunityId: queryParams.get('opportunityId'), + details: page.details, handle: state.auth.user ? state.auth.user.handle : '', isLoadingDetails: api.isLoadingDetails, phasesExpanded: page.phasesExpanded, @@ -184,6 +194,8 @@ const mapStateToProps = (state, ownProps) => { terms: terms.terms, termsFailure: terms.getTermsFailure, tokenV3: state.auth.tokenV3, + isLoggedIn: Boolean(state.auth.user), + isReviewer: _.includes((state.auth.user && state.auth.user.roles) || [], 'reviewer'), }; }; @@ -201,9 +213,9 @@ function mapDispatchToProps(dispatch) { dispatch(api.cancelApplicationsInit()); dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3)); }, - loadDetails: (challengeId, tokenV3) => { - dispatch(api.getDetailsInit()); - dispatch(api.getDetailsDone(challengeId, tokenV3)); + loadDetails: (challengeId, opportunityId, tokenV3) => { + dispatch(page.getDetailsInit()); + return dispatch(page.getDetailsDone(challengeId, opportunityId, tokenV3)); }, onPhaseExpand: () => dispatch(page.togglePhasesExpand()), openTermsModal: () => { @@ -211,9 +223,9 @@ function mapDispatchToProps(dispatch) { }, selectTab: tab => dispatch(page.selectTab(tab)), setRoles: roles => dispatch(page.setRoles(roles)), - submitApplications: (challengeId, roleIds, tokenV3) => { - dispatch(api.submitApplicationsInit()); - dispatch(api.submitApplicationsDone(challengeId, roleIds, tokenV3)); + submitApplications: (challengeId, tokenV3) => { + dispatch(page.submitApplicationsInit()); + return dispatch(page.submitApplicationsDone(challengeId, tokenV3)); }, toggleApplyModal: () => { dispatch(page.toggleApplyModal()); diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx new file mode 100644 index 0000000000..7770912fa0 --- /dev/null +++ b/src/shared/containers/SmartLooker.jsx @@ -0,0 +1,182 @@ +/** + * SmartLooker bridges legacy