Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -283,6 +283,7 @@
:global(.highcharts-container),
:global(.highcharts-root) {
height: 370px !important;
z-index: unset !important;
}
}

Expand Down Expand Up @@ -502,6 +503,8 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

color: #3877EA!important;
}

.tooltipWins {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -184,7 +184,7 @@ const StatisticsPage: FC = () => {
tabIndex={activeTab === 'winners' ? 0 : -1}
type='button'
>
Winners by Country
First place by Country
</button>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function renderWinnersTooltip(point: StatisticsMapPoint): string {
${flag}
<strong>${escapeHtml(point.name)}</strong>
</div>
<span>Winners: ${NUMBER_FORMATTER.format(point.value)}</span>
<span># of 1st place Wins: ${NUMBER_FORMATTER.format(point.value)}</span>
</div>
<span class="${styles.tooltipSectionTitle}">Top Winners</span>
<div class="${styles.tooltipWinners}">
Expand Down
19 changes: 19 additions & 0 deletions src/apps/work/src/lib/services/challenges.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
15 changes: 14 additions & 1 deletion src/apps/work/src/lib/services/challenges.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,19 @@ export async function deleteChallenge(challengeId: string): Promise<void> {
}

/**
* 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,
Expand All @@ -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<unknown>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -918,13 +918,18 @@ describe('HumanReviewTab', () => {
<TestHarness
defaultValues={{
reviewers: [],
timelineTemplateId: 'timeline-template-1',
}}
/>,
)

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)
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -1141,6 +1150,7 @@ describe('HumanReviewTab', () => {
shouldOpenOpportunity: true,
},
],
timelineTemplateId: 'two-round-timeline-template-id',
}}
screenerOnly
showReviewersValue
Expand All @@ -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 || '[]',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,10 @@ export const HumanReviewTab: FC<HumanReviewTabProps> = (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',
Expand Down Expand Up @@ -1564,6 +1568,7 @@ export const HumanReviewTab: FC<HumanReviewTabProps> = (props: HumanReviewTabPro
}, [selectedScorecardTrack, selectedScorecardType])

useEffect(() => {
const selectedTimelineTemplateId = timelineTemplateId?.trim() || ''
const selectedTypeId = typeId?.trim() || ''
const selectedTrackId = trackId?.trim() || ''

Expand All @@ -1574,7 +1579,11 @@ export const HumanReviewTab: FC<HumanReviewTabProps> = (props: HumanReviewTabPro

let mounted = true

fetchDefaultReviewers(selectedTypeId, selectedTrackId)
fetchDefaultReviewers({
timelineTemplateId: selectedTimelineTemplateId || undefined,
trackId: selectedTrackId,
typeId: selectedTypeId,
})
.then(fetchedDefaultReviewers => {
if (!mounted) {
return
Expand All @@ -1591,7 +1600,7 @@ export const HumanReviewTab: FC<HumanReviewTabProps> = (props: HumanReviewTabPro
return () => {
mounted = false
}
}, [trackId, typeId])
}, [timelineTemplateId, trackId, typeId])

useEffect(() => {
const activeReviewerTypeFieldNames = new Set<string>()
Expand Down
Loading