diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx index e2f97c816..872e6ea06 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentReview.tsx @@ -55,10 +55,115 @@ interface Props { isActiveChallenge: boolean } +const normalizeTabLabel = (value?: string): string => ( + value + ? value + .toLowerCase() + .replace(/[^a-z]/g, '') + : '' +) + +const parseScoreValue = (value: unknown): number | undefined => { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : undefined + } + + if (typeof value === 'string') { + const trimmed = value.trim() + if (!trimmed.length) { + return undefined + } + + const parsed = Number.parseFloat(trimmed) + return Number.isFinite(parsed) ? parsed : undefined + } + + return undefined +} + +const resolveSubmissionReviewScore = (submission: SubmissionInfo): number | undefined => { + const reviewResultScores = Array.isArray(submission.reviews) + ? submission.reviews + .map(review => parseScoreValue(review?.score)) + .filter((score): score is number => typeof score === 'number') + : [] + + if (reviewResultScores.length) { + const total = reviewResultScores.reduce((sum, score) => sum + score, 0) + return total / reviewResultScores.length + } + + const aggregateScore = parseScoreValue(submission.aggregateScore) + if (aggregateScore !== undefined) { + return aggregateScore + } + + const finalScore = parseScoreValue(submission.review?.finalScore) + if (finalScore !== undefined) { + return finalScore + } + + const initialScore = parseScoreValue(submission.review?.initialScore) + if (initialScore !== undefined) { + return initialScore + } + + return undefined +} + +type SubmissionScoreEntry = { + index: number + score?: number + submission: SubmissionInfo +} + +const sortSubmissionsByReviewScoreDesc = (rows: SubmissionInfo[]): SubmissionInfo[] => { + const entries: SubmissionScoreEntry[] = rows.map((submission, index) => ({ + index, + score: resolveSubmissionReviewScore(submission), + submission, + })) + + entries.sort((a: SubmissionScoreEntry, b: SubmissionScoreEntry) => { + const scoreA: number | undefined = a.score + const scoreB: number | undefined = b.score + const indexA: number = a.index + const indexB: number = b.index + + if (scoreA === undefined && scoreB === undefined) { + return indexA - indexB + } + + if (scoreA === undefined) { + return 1 + } + + if (scoreB === undefined) { + return -1 + } + + if (scoreB !== scoreA) { + return scoreB - scoreA + } + + return indexA - indexB + }) + + return entries.map(entry => entry.submission) +} + export const TabContentReview: FC = (props: Props) => { const selectedTab = props.selectedTab const providedReviews = props.reviews const providedSubmitterReviews = props.submitterReviews + const normalizedSelectedTab = useMemo( + () => normalizeTabLabel(selectedTab), + [selectedTab], + ) + const shouldSortReviewTabByScore = useMemo( + () => !props.isActiveChallenge && normalizedSelectedTab === 'review', + [normalizedSelectedTab, props.isActiveChallenge], + ) const { challengeInfo, challengeSubmissions: backendChallengeSubmissions, @@ -546,13 +651,27 @@ export const TabContentReview: FC = (props: Props) => { }, [resolvedSubmitterReviews], ) + const reviewerRowsForReviewTab = useMemo( + () => (shouldSortReviewTabByScore + ? sortSubmissionsByReviewScoreDesc(filteredReviews) + : filteredReviews), + [filteredReviews, shouldSortReviewTabByScore], + ) + const submitterRowsForReviewTab = useMemo( + () => (shouldSortReviewTabByScore + ? sortSubmissionsByReviewScoreDesc(filteredSubmitterReviews) + : filteredSubmitterReviews), + [filteredSubmitterReviews, shouldSortReviewTabByScore], + ) const hideHandleColumn = props.isActiveChallenge && actionChallengeRole === REVIEWER // show loading ui when fetching data const isSubmitterView = actionChallengeRole === SUBMITTER && selectedTab !== APPROVAL - const reviewRows = isSubmitterView ? filteredSubmitterReviews : filteredReviews + const reviewRows = isSubmitterView + ? (shouldSortReviewTabByScore ? submitterRowsForReviewTab : filteredSubmitterReviews) + : (shouldSortReviewTabByScore ? reviewerRowsForReviewTab : filteredReviews) if (props.isLoadingReview) { return @@ -596,14 +715,14 @@ export const TabContentReview: FC = (props: Props) => { return isSubmitterView ? ( ) : ( (name ? name.trim() + .toLowerCase() : '') +const SUBMISSION_PHASE_NAMES = new Set(['submission', 'topgear submission']) +const REGISTRATION_PHASE_NAME = 'registration' + // Helpers to keep UI hooks simple and under complexity limits // Compute a Set of this member's reviewer resource IDs (excluding iterative roles) @@ -1089,23 +1094,41 @@ export const ChallengeDetailsPage: FC = (props: Props) => { return allowed } - challengePhases.forEach(phase => { - if (!phase?.isOpen || !phase.predecessor) { + const addPhaseIdentifiers = (phase?: BackendPhase): void => { + if (!phase) { return } - allowed.add(phase.predecessor) + if (phase.id) { + allowed.add(phase.id) + } - const predecessorPhase = phaseLookup.get(phase.predecessor) - if (predecessorPhase?.id) { - allowed.add(predecessorPhase.id) + if (phase.phaseId) { + allowed.add(phase.phaseId) } + } - if (predecessorPhase?.phaseId) { - allowed.add(predecessorPhase.phaseId) + challengePhases.forEach(phase => { + if (!phase?.isOpen || !phase.predecessor) { + return } + + allowed.add(phase.predecessor) + addPhaseIdentifiers(phaseLookup.get(phase.predecessor)) }) + const hasSubmissionVariantOpen = challengePhases.some(phase => ( + phase?.isOpen && SUBMISSION_PHASE_NAMES.has(normalizePhaseName(phase.name)) + )) + + if (hasSubmissionVariantOpen) { + challengePhases.forEach(phase => { + if (normalizePhaseName(phase?.name) === REGISTRATION_PHASE_NAME) { + addPhaseIdentifiers(phase) + } + }) + } + return allowed }, [challengePhases, phaseLookup])