diff --git a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.module.scss b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.module.scss
index 087ba9099..782957589 100644
--- a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.module.scss
+++ b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.module.scss
@@ -283,6 +283,7 @@
:global(.highcharts-container),
:global(.highcharts-root) {
height: 370px !important;
+ z-index: unset !important;
}
}
@@ -502,6 +503,8 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+
+ color: #3877EA!important;
}
.tooltipWins {
diff --git a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.tsx b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.tsx
index e02db6ffe..22abc7001 100644
--- a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.tsx
+++ b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/StatisticsPage.tsx
@@ -73,7 +73,7 @@ const StatisticsPage: FC = () => {
? !generalStatistics && !generalStatisticsError
: !winners && !winnersError
const contentError = activeTab === 'countries' ? generalStatisticsError : winnersError
- const valueLabel = activeTab === 'countries' ? 'Members' : 'Winners'
+ const valueLabel = activeTab === 'countries' ? 'Members' : '1st Places'
const selectTab = useCallback((tab: StatisticsTab) => {
setActiveTab(tab)
@@ -184,7 +184,7 @@ const StatisticsPage: FC = () => {
tabIndex={activeTab === 'winners' ? 0 : -1}
type='button'
>
- Winners by Country
+ First place by Country
diff --git a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/WorldMap.tsx b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/WorldMap.tsx
index 7ce5e8092..8eae64042 100644
--- a/src/apps/customer-portal/src/pages/statistics/StatisticsPage/WorldMap.tsx
+++ b/src/apps/customer-portal/src/pages/statistics/StatisticsPage/WorldMap.tsx
@@ -113,7 +113,7 @@ function renderWinnersTooltip(point: StatisticsMapPoint): string {
${flag}
${escapeHtml(point.name)}
- Winners: ${NUMBER_FORMATTER.format(point.value)}
+ # of 1st place Wins: ${NUMBER_FORMATTER.format(point.value)}
Top Winners
diff --git a/src/apps/work/src/lib/services/challenges.service.spec.ts b/src/apps/work/src/lib/services/challenges.service.spec.ts
index ef70a3131..48fa46836 100644
--- a/src/apps/work/src/lib/services/challenges.service.spec.ts
+++ b/src/apps/work/src/lib/services/challenges.service.spec.ts
@@ -126,6 +126,25 @@ describe('fetchDefaultReviewers', () => {
},
])
})
+
+ it('requests defaults for the selected timeline template', async () => {
+ const mockedGet = xhrGetAsync as jest.Mock
+
+ mockedGet.mockResolvedValue([])
+
+ await fetchDefaultReviewers({
+ timelineTemplateId: ' timeline-template-1 ',
+ trackId: ' track-1 ',
+ typeId: ' type-1 ',
+ })
+
+ expect(mockedGet)
+ .toHaveBeenCalledWith(
+ 'https://example.com/default-reviewers'
+ + '?typeId=type-1&trackId=track-1&timelineTemplateId=timeline-template-1',
+ expect.any(Object),
+ )
+ })
})
describe('patchChallenge', () => {
diff --git a/src/apps/work/src/lib/services/challenges.service.ts b/src/apps/work/src/lib/services/challenges.service.ts
index fad44bfea..e178f1c6f 100644
--- a/src/apps/work/src/lib/services/challenges.service.ts
+++ b/src/apps/work/src/lib/services/challenges.service.ts
@@ -688,10 +688,19 @@ export async function deleteChallenge(challengeId: string): Promise {
}
/**
- * Fetch default reviewers metadata.
+ * Fetches default reviewer metadata for a challenge configuration.
+ *
+ * @param typeIdOrFilters challenge type id for the legacy positional call, or type, track,
+ * and timeline-template filters for a template-specific lookup.
+ * @param trackId challenge track id used with the legacy positional call.
+ * @returns the normalized default reviewer rows matching the supplied configuration.
+ * @remarks The reviewer editor uses the filter-object form so challenges with multiple timeline
+ * templates receive the scorecards and reviewer phases configured for the selected template.
+ * @throws a normalized request error when the default-reviewer endpoint cannot be reached.
*/
export async function fetchDefaultReviewers(
typeIdOrFilters: string | {
+ timelineTemplateId?: string
trackId?: string
typeId?: string
} | undefined,
@@ -713,6 +722,10 @@ export async function fetchDefaultReviewers(
query.set('trackId', filters.trackId.trim())
}
+ if (filters.timelineTemplateId?.trim()) {
+ query.set('timelineTemplateId', filters.timelineTemplateId.trim())
+ }
+
try {
const queryString = query.toString()
const response = await xhrGetAsync(
diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md
index ef6c0948c..e4f0beb54 100644
--- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md
+++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md
@@ -85,7 +85,7 @@ The form uses `challengeBasicInfoSchema` from `src/apps/work/src/lib/schemas/cha
from `is_test_challenge`, and explicitly persists metadata value `true` or `false`. Test
challenges do not generate payments, and authorized modifiers can delete them after they reach
a completed or cancelled status.
-- `ReviewersField`: hidden for `Task` and `Marathon Match` challenges because manual reviewer assignment is handled elsewhere. The simplified Design Challenge review section repairs missing, duplicate, and stale hidden reviewer rows from the active defaults while exposing the Screening and Checkpoint Screening member selectors. Checkpoint Review, Review, and Approval are private and automatically assigned to the selected copilot during save; Design Challenge creation and saving highlight the Copilot field when no copilot is selected. On the full human-review tab, each manual reviewer card keeps the legacy review-type dropdown, backfills missing legacy review-type values from the matching default reviewer or iterative-review phase fallback, and each manual reviewer phase selector hides registration/submission phases and any phase already assigned on another manual reviewer card while preserving the card's current selection. When default reviewer metadata is missing, stale, or already covered by existing rows, `Add reviewer` starts from the next unassigned selectable reviewer phase, preferring review phases before approval or screening phases, so single-round Design schedules add the Approver row instead of a registration/submission or duplicate reviewer row. Manual reviewer counts are capped before rendering member assignment controls so closed public opportunities cannot create an unbounded number of member selectors. The full Design reviewer editor keeps the public review opportunity checkbox disabled and unchecked. Screening and Checkpoint Screening member selectors remain available but are optional so a copilot can assign the Screener or Checkpoint Screener after launch.
+- `ReviewersField`: hidden for `Task` and `Marathon Match` challenges because manual reviewer assignment is handled elsewhere. The simplified Design Challenge review section fetches defaults for the selected timeline template, then repairs missing, duplicate, and stale hidden reviewer rows while exposing the Screening and Checkpoint Screening member selectors. Checkpoint Review, Review, and Approval are private and automatically assigned to the selected copilot during save; Design Challenge creation and saving highlight the Copilot field when no copilot is selected. On the full human-review tab, each manual reviewer card keeps the legacy review-type dropdown, backfills missing legacy review-type values from the matching default reviewer or iterative-review phase fallback, and each manual reviewer phase selector hides registration/submission phases and any phase already assigned on another manual reviewer card while preserving the card's current selection. When default reviewer metadata is missing, stale, or already covered by existing rows, `Add reviewer` starts from the next unassigned selectable reviewer phase, preferring review phases before approval or screening phases, so single-round Design schedules add the Approver row instead of a registration/submission or duplicate reviewer row. Manual reviewer counts are capped before rendering member assignment controls so closed public opportunities cannot create an unbounded number of member selectors. The full Design reviewer editor keeps the public review opportunity checkbox disabled and unchecked. Screening and Checkpoint Screening member selectors remain available but are optional so a copilot can assign the Screener or Checkpoint Screener after launch.
- `Submission Settings`: shown for Design `Challenge` and Design `First2Finish` types, and contains the final-deliverables, stock-art, and submission-limit compatibility fields.
- `RegisteredMemberDownloadField`: shown in Advanced Options for every created challenge type. The radio group persists `allowAllRegistrantsToDownloadWinningSubmissions` as the exact string `true` for all challenge registrants or `false` for passing submitters only. New Development challenges default to passing submitters; other new challenges, including Design, default to all registrants. Existing challenges without the metadata retain passing-submitter-only access.
- `FinalDeliverablesField`: design-challenge file-type editor that persists the legacy `fileTypes` metadata payload used on challenge draft pages.
diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.spec.tsx
index da8966dfe..b99deb345 100644
--- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.spec.tsx
+++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.spec.tsx
@@ -918,13 +918,18 @@ describe('HumanReviewTab', () => {
,
)
await waitFor(() => {
expect(mockedFetchDefaultReviewers)
- .toHaveBeenCalledWith('type-1', 'track-1')
+ .toHaveBeenCalledWith({
+ timelineTemplateId: 'timeline-template-1',
+ trackId: 'track-1',
+ typeId: 'type-1',
+ })
})
await waitFor(() => {
expect((screen.getByRole('button', { name: 'Add reviewer' }) as HTMLButtonElement).disabled)
@@ -1013,43 +1018,47 @@ describe('HumanReviewTab', () => {
isLoading: false,
resourceRoles: [],
})
- mockedFetchDefaultReviewers.mockResolvedValue([
- {
- isMemberReview: true,
- memberReviewerCount: 1,
- phaseId: 'checkpoint-review-phase-id',
- scorecardId: 'checkpoint-review-scorecard-id',
- shouldOpenOpportunity: false,
- },
- {
- isMemberReview: true,
- memberReviewerCount: 1,
- phaseId: 'review-phase-id',
- scorecardId: 'review-scorecard-id',
- shouldOpenOpportunity: false,
- },
- {
- isMemberReview: true,
- memberReviewerCount: 1,
- phaseId: 'checkpoint-screening-phase-id',
- scorecardId: 'checkpoint-screening-scorecard-id',
- shouldOpenOpportunity: false,
- },
- {
- isMemberReview: true,
- memberReviewerCount: 1,
- phaseId: 'screening-phase-id',
- scorecardId: 'screening-scorecard-id',
- shouldOpenOpportunity: false,
- },
- {
- isMemberReview: true,
- memberReviewerCount: 1,
- phaseId: 'approval-phase-id',
- scorecardId: 'approval-scorecard-id',
- shouldOpenOpportunity: false,
- },
- ])
+ mockedFetchDefaultReviewers.mockImplementation(filters => Promise.resolve(
+ filters?.timelineTemplateId === 'two-round-timeline-template-id'
+ ? [
+ {
+ isMemberReview: true,
+ memberReviewerCount: 1,
+ phaseId: 'checkpoint-review-phase-id',
+ scorecardId: 'checkpoint-review-scorecard-id',
+ shouldOpenOpportunity: false,
+ },
+ {
+ isMemberReview: true,
+ memberReviewerCount: 1,
+ phaseId: 'review-phase-id',
+ scorecardId: 'review-scorecard-id',
+ shouldOpenOpportunity: false,
+ },
+ {
+ isMemberReview: true,
+ memberReviewerCount: 1,
+ phaseId: 'checkpoint-screening-phase-id',
+ scorecardId: 'checkpoint-screening-scorecard-id',
+ shouldOpenOpportunity: false,
+ },
+ {
+ isMemberReview: true,
+ memberReviewerCount: 1,
+ phaseId: 'screening-phase-id',
+ scorecardId: 'screening-scorecard-id',
+ shouldOpenOpportunity: false,
+ },
+ {
+ isMemberReview: true,
+ memberReviewerCount: 1,
+ phaseId: 'approval-phase-id',
+ scorecardId: 'approval-scorecard-id',
+ shouldOpenOpportunity: false,
+ },
+ ]
+ : [],
+ ))
mockedFetchScorecards.mockResolvedValue([
{
id: 'checkpoint-review-scorecard-id',
@@ -1141,6 +1150,7 @@ describe('HumanReviewTab', () => {
shouldOpenOpportunity: true,
},
],
+ timelineTemplateId: 'two-round-timeline-template-id',
}}
screenerOnly
showReviewersValue
@@ -1149,6 +1159,14 @@ describe('HumanReviewTab', () => {
expect(screen.getByLabelText('Screener'))
.not.toBeNull()
+ await waitFor(() => {
+ expect(mockedFetchDefaultReviewers)
+ .toHaveBeenCalledWith({
+ timelineTemplateId: 'two-round-timeline-template-id',
+ trackId: 'track-1',
+ typeId: 'type-1',
+ })
+ })
await waitFor(() => {
const reconciledReviewers = JSON.parse(
screen.getByTestId('reviewers-value').textContent || '[]',
diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.tsx
index f74401bba..f0be18574 100644
--- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.tsx
+++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/HumanReviewTab.tsx
@@ -1186,6 +1186,10 @@ export const HumanReviewTab: FC = (props: HumanReviewTabPro
control: formContext.control,
name: 'typeId',
}) as string | undefined
+ const timelineTemplateId = useWatch({
+ control: formContext.control,
+ name: 'timelineTemplateId',
+ }) as string | undefined
const prizeSets = useWatch({
control: formContext.control,
name: 'prizeSets',
@@ -1564,6 +1568,7 @@ export const HumanReviewTab: FC = (props: HumanReviewTabPro
}, [selectedScorecardTrack, selectedScorecardType])
useEffect(() => {
+ const selectedTimelineTemplateId = timelineTemplateId?.trim() || ''
const selectedTypeId = typeId?.trim() || ''
const selectedTrackId = trackId?.trim() || ''
@@ -1574,7 +1579,11 @@ export const HumanReviewTab: FC = (props: HumanReviewTabPro
let mounted = true
- fetchDefaultReviewers(selectedTypeId, selectedTrackId)
+ fetchDefaultReviewers({
+ timelineTemplateId: selectedTimelineTemplateId || undefined,
+ trackId: selectedTrackId,
+ typeId: selectedTypeId,
+ })
.then(fetchedDefaultReviewers => {
if (!mounted) {
return
@@ -1591,7 +1600,7 @@ export const HumanReviewTab: FC = (props: HumanReviewTabPro
return () => {
mounted = false
}
- }, [trackId, typeId])
+ }, [timelineTemplateId, trackId, typeId])
useEffect(() => {
const activeReviewerTypeFieldNames = new Set()