Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
425f563
PM-5203 - allow manager to update ai score
vas3a Jun 2, 2026
f42a7a9
lint
vas3a Jun 2, 2026
5b5f4b0
lint
vas3a Jun 2, 2026
8323301
PM-5203 - manager score override
vas3a Jun 2, 2026
6d70d38
lint
vas3a Jun 2, 2026
bc0becd
PM-5203 - filter latest submissions
vas3a Jun 3, 2026
c974999
lint
vas3a Jun 3, 2026
f95d972
Improve UI for approval tab content
vas3a Jun 3, 2026
799d1f9
PM-5203 - approval UI
vas3a Jun 3, 2026
65ffbc1
lint
vas3a Jun 3, 2026
84f686e
PM-5203 - More UI improvements for AI approval
vas3a Jun 3, 2026
9196cb3
Final score for ai only challenges in work manager
vas3a Jun 4, 2026
7d29c22
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into P…
vas3a Jun 4, 2026
c0647ab
Final score in work manager submissions
vas3a Jun 4, 2026
8d9bfe0
PM-5203 - Update UI for approval tab
vas3a Jun 4, 2026
45753a2
lint
vas3a Jun 4, 2026
70d1859
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into P…
vas3a Jun 4, 2026
7ae5350
keep "override" label after re-run
vas3a Jun 4, 2026
836e68b
show error if manual reviewers aren't set
vas3a Jun 4, 2026
c0b3834
check initial score
vas3a Jun 4, 2026
a6f2f2a
lint
vas3a Jun 4, 2026
40eaae2
Merge pull request #1899 from topcoder-platform/PM-5203_approval-phas…
vas3a Jun 5, 2026
8d1bb5b
Expose marathon compiler issues
jmgasper Jun 7, 2026
2086b96
Merge pull request #1908 from topcoder-platform/marathon-compiler-fixes
jmgasper Jun 7, 2026
906553e
Update message for no reviewer
vas3a Jun 8, 2026
6fd59b4
Allow inline score editing in ai scorecard view in approval phase
vas3a Jun 8, 2026
c1c86af
Remove "edit" in approval tab. this will happen in scorcard viewer
vas3a Jun 8, 2026
f34676e
Allow score edit only during approval
vas3a Jun 8, 2026
d4d2f4f
Use the richtext editor for manager comment
vas3a Jun 8, 2026
63d917b
lint
vas3a Jun 8, 2026
0979ddd
Merge pull request #1910 from topcoder-platform/PM-5203_approval-phas…
vas3a Jun 8, 2026
c8a812d
PM-5282: Preserve schedule phase ids on update
jmgasper Jun 9, 2026
5b1e481
Merge pull request #1911 from topcoder-platform/PM-5282
jmgasper Jun 9, 2026
6fa46f5
PM-5243: Add reviewer with next valid default phase
jmgasper Jun 9, 2026
d912181
Merge pull request #1912 from topcoder-platform/PM-5243
jmgasper Jun 9, 2026
356ec4d
Fix for hang in work app on 1 round design challenges (PM-5243)
jmgasper Jun 9, 2026
db75484
PM-5265: Add awards see less toggle
jmgasper Jun 9, 2026
b617be3
Merge remote-tracking branch 'origin/ai-ratings' into PM-5265
jmgasper Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import '@testing-library/jest-dom'
import type { PropsWithChildren, ReactNode } from 'react'
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'

import { useMemberBadges, type UserBadge, type UserBadgesResponse, type UserProfile } from '~/libs/core'

Expand Down Expand Up @@ -32,25 +32,32 @@ jest.mock('../../components', () => ({

const mockUseMemberBadges = useMemberBadges as jest.MockedFunction<typeof useMemberBadges>

function createBadge(badgeName: string): UserBadge {
/**
* Builds a profile award fixture for CommunityAwards tests. The badge name and
* index are used to create stable display text and IDs, it returns a UserBadge
* accepted by the component, and it does not raise exceptions.
*/
function createBadge(badgeName: string, index: number = 1): UserBadge {
const badgeId: string = `badge-${index}`

return {
awarded_at: new Date('2026-06-04T00:00:00.000Z'),
awarded_by: 'admin',
org_badge: {
active: true,
badge_description: 'Awarded for AI profile work.',
badge_image_url: 'https://example.com/ai-rookie.svg',
badge_image_url: `https://example.com/${badgeId}.svg`,
badge_name: badgeName,
badge_status: 'active',
id: 'badge-1',
id: badgeId,
organization_id: 'topcoder',
orgranization: {
id: 'topcoder',
name: 'Topcoder',
},
tags_id_tags: [],
},
org_badge_id: 'badge-1',
org_badge_id: badgeId,
user_handle: 'tester',
user_id: '123',
}
Expand Down Expand Up @@ -83,4 +90,38 @@ describe('CommunityAwards', () => {
expect(mockUseMemberBadges)
.toHaveBeenCalledWith(123, { limit: 500 })
})

it('lets members expand awards and collapse them back to the default view', () => {
const memberBadges: UserBadgesResponse = {
count: 6,
rows: Array.from({ length: 6 }, (_, index) => createBadge(`AI Award ${index + 1}`, index + 1)),
}

mockUseMemberBadges.mockReturnValue(memberBadges)

render(<CommunityAwards profile={{ userId: 123 } as UserProfile} />)

expect(screen.getByRole('button', { name: 'View AI Award 1 award details' }))
.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'View AI Award 4 award details' }))
.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'View AI Award 5 award details' }))
.not
.toBeInTheDocument()

fireEvent.click(screen.getByRole('button', { name: '+ 2 more badges' }))

expect(screen.getByRole('button', { name: 'View AI Award 5 award details' }))
.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'See less' }))
.toBeInTheDocument()

fireEvent.click(screen.getByRole('button', { name: 'See less' }))

expect(screen.queryByRole('button', { name: 'View AI Award 5 award details' }))
.not
.toBeInTheDocument()
expect(screen.getByRole('button', { name: '+ 2 more badges' }))
.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,20 @@ const CommunityAwards: FC<CommunityAwardsProps> = (props: CommunityAwardsProps)
setIsAwardsExpanded(false)
}, [props.profile?.userId])

function handleAwardsExpandClick(): void {
setIsAwardsExpanded(true)
/**
* Toggles the awards section between the collapsed four-badge preview and
* the expanded list. It is used by the more/less control, takes no
* parameters, returns nothing, and does not raise exceptions.
*/
function handleAwardsToggleClick(): void {
setIsAwardsExpanded(isExpanded => !isExpanded)
}

/**
* Closes the selected badge details modal. It is passed to
* MemberBadgeModal, takes no parameters, returns nothing, and does not
* raise exceptions.
*/
function handleMemberBadgeModalClose(): void {
setIsBadgeDetailsOpen(false)
}
Expand Down Expand Up @@ -82,13 +92,15 @@ const CommunityAwards: FC<CommunityAwardsProps> = (props: CommunityAwardsProps)
}
</div>

{!isAwardsExpanded && additionalBadgeCount > 0 && (
{additionalBadgeCount > 0 && (
<button
className={styles.moreBadgesButton}
onClick={handleAwardsExpandClick}
onClick={handleAwardsToggleClick}
type='button'
>
{`+ ${additionalBadgeCount} more ${additionalBadgeCount === 1 ? 'badge' : 'badges'}`}
{isAwardsExpanded
? 'See less'
: `+ ${additionalBadgeCount} more ${additionalBadgeCount === 1 ? 'badge' : 'badges'}`}
</button>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,51 @@
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255,255,255,0.3);
}
}

// Score with inline override
.scoreWithOverride {
display: flex;
align-items: center;
gap: 8px;
}

.originalScore {
color: $black-40;
font-size: 12px;
}

.overrideInput {
width: 70px;
padding: 4px 6px;
border: 1px solid $link-blue-dark;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
text-align: center;

&:focus {
outline: none;
border-color: $link-blue-dark;
box-shadow: 0 0 0 2px rgba(13, 105, 212, 0.15);
}

&::placeholder {
color: $black-40;
font-size: 10px;
}
}

.overriddenScore {
display: flex;
align-items: center;
gap: 4px;
color: $orange-120;
font-weight: 600;
}

.overrideLabel {
font-size: 11px;
font-weight: 400;
color: $black-40;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable complexity */
/* eslint-disable max-len */
import { FC, MouseEvent as ReactMouseEvent, useCallback, useContext, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
Expand Down Expand Up @@ -42,11 +43,18 @@ import styles from './AiReviewsTable.module.scss'
interface AiReviewsTableProps {
submission: Pick<BackendSubmission, 'id'|'virusScan'>
aiReviewers?: { aiWorkflowId: string }[]
/** Enable editing mode for manager score overrides */
editMode?: boolean
/** Current edited scores by workflowId */
editedScores?: Record<string, string>
/** Callback when a score is changed */
onScoreChange?: (workflowId: string, value: string) => void
}

interface AiReviewerRow {
id: string
isGating?: boolean
managerScore?: number | null
minScore?: number
reviewDate?: string
run?: Pick<AiWorkflowRun, 'id'|'score'|'status'|'workflow'>
Expand Down Expand Up @@ -249,16 +257,17 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
const status = fromDecision
? normalizeStatus(run && aiRunInProgress(run)
? undefined
: fromDecision.runStatus, fromDecision.runScore, minScore)
: fromDecision.runStatus, fromDecision.managerScore ?? fromDecision.runScore, minScore)
: undefined

return {
id: workflowId,
isGating: fromDecision?.isGating ?? configured?.isGating,
managerScore: fromDecision?.managerScore ?? (run?.initialScore !== null && run?.initialScore !== undefined ? run.score : undefined),
minScore,
reviewDate: run?.completedAt,
run,
score: fromDecision?.runScore ?? run?.score,
score: fromDecision?.managerScore ?? fromDecision?.runScore ?? run?.score,
status,
title: getConfiguredWorkflowName(configured?.workflow) ?? run?.workflow?.name ?? 'AI Review',
weight: fromDecision?.weightPercent ?? configured?.weightPercent,
Expand Down Expand Up @@ -502,13 +511,40 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
<div className={styles.mobileRow}>
<div className={styles.label}>Score</div>
<div className={styles.value}>
{typeof row.score === 'number' ? (
{row.workflowId && props.editMode && props.onScoreChange ? (
<div className={styles.scoreWithOverride}>
<span className={styles.originalScore}>
{typeof row.score === 'number' ? formatScore(row.score) : '-'}
</span>
<input
type='number'
step='0.01'
className={styles.overrideInput}
value={props.editedScores?.[row.workflowId] ?? ''}
onChange={function onChange(
e: React.ChangeEvent<HTMLInputElement>,
) {
if (props.onScoreChange && row.workflowId) {
props.onScoreChange(row.workflowId, e.target.value)
}
}}
placeholder='Override'
/>
</div>
) : typeof row.score === 'number' ? (
row.workflowId ? (
<Link
to={`../reviews/${props.submission.id}?workflowId=${row.workflowId}`}
>
{formatScore(row.score)}
</Link>
<>
<Link
to={`../reviews/${props.submission.id}?workflowId=${row.workflowId}`}
>
{formatScore(row.score)}
</Link>
{row.managerScore !== null && row.managerScore !== undefined && (
<span className={styles.overriddenScore}>
<span className={styles.overrideLabel}>(override)</span>
</span>
)}
</>
) : formatScore(row.score)
) : '-'}
</div>
Expand Down Expand Up @@ -612,13 +648,40 @@ const AiReviewsTable: FC<AiReviewsTableProps> = props => {
)}
</td>
<td className={styles.scoreCol}>
{typeof row.score === 'number' ? (
{row.workflowId && props.editMode && props.onScoreChange ? (
<div className={styles.scoreWithOverride}>
<span className={styles.originalScore}>
{typeof row.score === 'number' ? formatScore(row.score) : '-'}
</span>
<input
type='number'
step='0.01'
className={styles.overrideInput}
value={props.editedScores?.[row.workflowId] ?? ''}
onChange={function onChange(
e: React.ChangeEvent<HTMLInputElement>,
) {
if (props.onScoreChange && row.workflowId) {
props.onScoreChange(row.workflowId, e.target.value)
}
}}
placeholder='Override'
/>
</div>
) : typeof row.score === 'number' ? (
row.workflowId ? (
<Link
to={`../reviews/${props.submission.id}?workflowId=${row.workflowId}`}
>
{formatScore(row.score)}
</Link>
<>
<Link
to={`../reviews/${props.submission.id}?workflowId=${row.workflowId}`}
>
{formatScore(row.score)}
</Link>
{row.managerScore !== null && row.managerScore !== undefined && (
<span className={styles.overriddenScore}>
<span className={styles.overrideLabel}>(override)</span>
</span>
)}
</>
) : formatScore(row.score)
) : '-'}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ import {
ScorecardQuestion,
SelectOption,
} from '../../models'
import { formAppealResponseSchema, isAppealsResponsePhase } from '../../utils'
import {
QUESTION_YES_NO_OPTIONS,
} from '../../../config/index.config'
import { formAppealResponseSchema, getScoreResponseOptions, isAppealsResponsePhase } from '../../utils'
import { ChallengeDetailContext } from '../../contexts'

import styles from './AppealComment.module.scss'
Expand Down Expand Up @@ -103,28 +100,9 @@ export const AppealComment: FC<Props> = (props: Props) => {
}
}, [addAppealResponse, appealInfo, reviewItem, updatedResponse])

const responseOptions = useMemo<SelectOption[]>(() => {
if (scorecardQuestion.type === 'SCALE') {
const length
= scorecardQuestion.scaleMax
- scorecardQuestion.scaleMin
+ 1
return Array.from(
new Array(length),
(x, i) => `${i + scorecardQuestion.scaleMin}`,
)
.map(item => ({
label: item,
value: item,
}))
}

if (scorecardQuestion.type === 'YES_NO') {
return QUESTION_YES_NO_OPTIONS
}

return []
}, [scorecardQuestion])
const responseOptions = useMemo<SelectOption[]>(() => (
getScoreResponseOptions(scorecardQuestion)
), [scorecardQuestion])

useEffect(() => {
setAppealResponse(data.appealResponse?.content ?? '')
Expand Down
Loading
Loading