diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap index ab31d14b9..d8f5971b3 100644 --- a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap @@ -30,6 +30,7 @@ exports[`Matches shallow shapshot 1`] = ` "id": "test-challenge", } } + isWorkflowRunComplete={true} onDelete={[Function]} onDownload={[Function]} onOpenDownloadArtifactsModal={[Function]} diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx index 93829d147..2c15defb4 100644 --- a/src/shared/components/SubmissionManagement/Submission/index.jsx +++ b/src/shared/components/SubmissionManagement/Submission/index.jsx @@ -42,6 +42,7 @@ export default function Submission(props) { onOpenRatingsListModal, status, allowDelete, + isWorkflowRunComplete, } = props; const formatDate = date => moment(+new Date(date)).format('MMM DD, YYYY hh:mm A'); const onDownloadSubmission = onDownload.bind(1, submissionObject.id); @@ -66,6 +67,10 @@ export default function Submission(props) { } } + const showDeleteButton = status !== CHALLENGE_STATUS.COMPLETED + && track === COMPETITION_TRACKS.DES + && safeForDownloadCheck === true; + return ( @@ -146,17 +151,33 @@ export default function Submission(props) { onClick={() => onDownload(submissionObject.id)} > */ } - {status !== CHALLENGE_STATUS.COMPLETED - && track === COMPETITION_TRACKS.DES - && safeForDownloadCheck === true && ( - + {showDeleteButton && ( + isWorkflowRunComplete ? ( + + ) : ( + // Disabled delete button with tooltip when workflow run is pending + ( +
+ You can delete this submission only after the review is complete. +
+ )} + > + +
+ ) ) } { !isTopCrowdChallenge @@ -217,4 +238,5 @@ Submission.propTypes = { allowDelete: PT.bool.isRequired, onOpenDownloadArtifactsModal: PT.func, onOpenRatingsListModal: PT.func, + isWorkflowRunComplete: PT.bool.isRequired, }; diff --git a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx index 9f36dfedd..a9ca5652f 100644 --- a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx @@ -74,9 +74,29 @@ export default function SubmissionsTable(props) { submissionObjects.forEach((subObject) => { // submissionPhaseStartDate will be the start date of // the current submission/checkpoint or empty string if any other phase + + const TERMINAL_STATUSES = [ + 'COMPLETED', + 'FAILURE', + 'CANCELLED', + 'SUCCESS', + ]; + + const workflowRunsForSubmission = submissionWorkflowRuns + && submissionWorkflowRuns[subObject.id] + ? submissionWorkflowRuns[subObject.id] + : null; + + const hasRuns = workflowRunsForSubmission && workflowRunsForSubmission.length > 0; + + const isWorkflowRunComplete = !hasRuns + ? true + : workflowRunsForSubmission.every(run => TERMINAL_STATUSES.includes(run.status)); + const allowDelete = submissionPhaseStartDate && moment(subObject.submissionDate).isAfter(submissionPhaseStartDate); + const submission = ( ); submissionsWithDetails.push(submission); - const workflowRunsForSubmission = submissionWorkflowRuns - && submissionWorkflowRuns[subObject.id] - ? submissionWorkflowRuns[subObject.id] - : null; const submissionDetail = ( diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index d9e525e65..6fcbd7ffd 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -1,4 +1,5 @@ import { config } from 'topcoder-react-utils'; +import { withEstimatedReviewerPayments } from 'utils/reviewOpportunities'; const v6ApiUrl = config.API.V6; @@ -22,7 +23,10 @@ export default async function getReviewOpportunities(page, pageSize) { throw new Error(res.statusText); } - return res.json(); + const data = await res.json(); + + const opportunities = Array.isArray(data) ? data : []; + return opportunities.map(opportunity => withEstimatedReviewerPayments(opportunity)); } /** @@ -51,7 +55,7 @@ export async function getDetails(challengeId, opportunityId) { const challengeData = await challengeRes.json(); return { - ...opportunityData.result.content, + ...withEstimatedReviewerPayments(opportunityData.result.content), challenge: challengeData, }; } catch (err) { diff --git a/src/shared/utils/reviewOpportunities.js b/src/shared/utils/reviewOpportunities.js index f2731bdd2..c92fbeea0 100644 --- a/src/shared/utils/reviewOpportunities.js +++ b/src/shared/utils/reviewOpportunities.js @@ -5,6 +5,54 @@ import _ from 'lodash'; import moment from 'moment'; import { REVIEW_OPPORTUNITY_TYPES } from './tc'; +export const DEFAULT_ESTIMATED_SUBMISSIONS = 2; + +export const calculateEstimatedReviewerPayment = ( + basePayment, + incrementalPayment, + estimatedSubmissions = DEFAULT_ESTIMATED_SUBMISSIONS, +) => { + const base = _.toNumber(basePayment); + const incremental = _.toNumber(incrementalPayment); + const submissions = _.toNumber(estimatedSubmissions); + + if (_.isNaN(base) || _.isNaN(submissions)) { + return null; + } + + const incrementalValue = _.isNaN(incremental) ? 0 : incremental; + return base + (submissions * incrementalValue); +}; + +export const withEstimatedReviewerPayments = ( + opportunity, + estimatedSubmissions = DEFAULT_ESTIMATED_SUBMISSIONS, +) => { + if (!opportunity) return opportunity; + + const estimatedPayment = calculateEstimatedReviewerPayment( + _.get(opportunity, 'basePayment', _.get(opportunity, 'payments[0].payment')), + _.get(opportunity, 'incrementalPayment', 0), + estimatedSubmissions, + ); + + if (!_.isNumber(estimatedPayment)) { + return opportunity; + } + + const payments = Array.isArray(opportunity.payments) + ? opportunity.payments.map(payment => ({ + ...payment, + payment: estimatedPayment, + })) + : opportunity.payments; + + return { + ...opportunity, + payments, + }; +}; + /** * Infers open positions using review opportunity details and organizes them by role * @@ -14,7 +62,10 @@ import { REVIEW_OPPORTUNITY_TYPES } from './tc'; export const openPositionsByRole = (details) => { if (!details.payments) return []; - const roleCount = details.payments.length; + const detailsWithPayments = withEstimatedReviewerPayments(details); + const payments = detailsWithPayments.payments || []; + + const roleCount = payments.length; let approved; if (details.applications && details.openPositions === 1 && roleCount === 2) { @@ -28,7 +79,7 @@ export const openPositionsByRole = (details) => { return details.openPositions / roleCount; }; - return details.payments.map(({ role, roleId, payment }) => ({ + return payments.map(({ role, roleId, payment }) => ({ role, roleId, payment,