diff --git a/src/apps/work/src/lib/models/Reviewer.model.ts b/src/apps/work/src/lib/models/Reviewer.model.ts index 9c2cbb799..80b53835b 100644 --- a/src/apps/work/src/lib/models/Reviewer.model.ts +++ b/src/apps/work/src/lib/models/Reviewer.model.ts @@ -44,6 +44,7 @@ export interface DefaultReviewer { memberReviewerCount?: number opportunityType?: string phaseId?: string + phaseName?: string roleId?: string scorecardId?: string shouldOpenOpportunity?: boolean 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 48fa46836..12c417b75 100644 --- a/src/apps/work/src/lib/services/challenges.service.spec.ts +++ b/src/apps/work/src/lib/services/challenges.service.spec.ts @@ -107,7 +107,7 @@ describe('fetchDefaultReviewers', () => { { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'phase-1', + phaseName: 'Review', roleId: 'role-1', scorecardId: 'scorecard-1', shouldOpenOpportunity: true, @@ -119,7 +119,7 @@ describe('fetchDefaultReviewers', () => { { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'phase-1', + phaseName: 'Review', roleId: 'role-1', scorecardId: 'scorecard-1', shouldOpenOpportunity: true, diff --git a/src/apps/work/src/lib/services/challenges.service.ts b/src/apps/work/src/lib/services/challenges.service.ts index e178f1c6f..564f4c2fe 100644 --- a/src/apps/work/src/lib/services/challenges.service.ts +++ b/src/apps/work/src/lib/services/challenges.service.ts @@ -414,6 +414,7 @@ function normalizeDefaultReviewer( opportunityType: toOptionalString((reviewer as Record).opportunityType) || toOptionalString((reviewer as Record).type), phaseId: toOptionalString(reviewer.phaseId), + phaseName: toOptionalString(reviewer.phaseName), roleId: toOptionalString(reviewer.roleId), scorecardId: toOptionalString((reviewer as Record).scorecardId), shouldOpenOpportunity: toOptionalBoolean((reviewer as Record).shouldOpenOpportunity), diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md index e4f0beb54..c1dce85ea 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 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. +- `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, resolves the API's phase-name-only defaults against the challenge phases, 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 b99deb345..9f7ebbe3e 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 @@ -1006,7 +1006,7 @@ describe('HumanReviewTab', () => { .not.toBeNull() }) - it('repairs hidden legacy rows and assigns private reviewers to the copilot', async () => { + it('resolves phase-name-only defaults and assigns private reviewers to the copilot', async () => { mockedUseFetchChallengeTracks.mockReturnValue({ tracks: [{ id: 'track-1', @@ -1024,35 +1024,35 @@ describe('HumanReviewTab', () => { { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'checkpoint-review-phase-id', + phaseName: 'Checkpoint Review', scorecardId: 'checkpoint-review-scorecard-id', shouldOpenOpportunity: false, }, { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'review-phase-id', + phaseName: 'Review', scorecardId: 'review-scorecard-id', shouldOpenOpportunity: false, }, { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'checkpoint-screening-phase-id', + phaseName: 'Checkpoint Screening', scorecardId: 'checkpoint-screening-scorecard-id', shouldOpenOpportunity: false, }, { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'screening-phase-id', + phaseName: 'Screening', scorecardId: 'screening-scorecard-id', shouldOpenOpportunity: false, }, { isMemberReview: true, memberReviewerCount: 1, - phaseId: 'approval-phase-id', + phaseName: 'Approval', scorecardId: 'approval-scorecard-id', shouldOpenOpportunity: false, }, @@ -1063,27 +1063,27 @@ describe('HumanReviewTab', () => { { id: 'checkpoint-review-scorecard-id', name: 'Checkpoint Review Scorecard', - phaseId: 'checkpoint-review-phase-id', + type: 'CHECKPOINT_REVIEW', }, { id: 'review-scorecard-id', name: 'Review Scorecard', - phaseId: 'review-phase-id', + type: 'REVIEW', }, { id: 'checkpoint-screening-scorecard-id', name: 'Checkpoint Screening Scorecard', - phaseId: 'checkpoint-screening-phase-id', + type: 'CHECKPOINT_SCREENING', }, { id: 'screening-scorecard-id', name: 'Screening Scorecard', - phaseId: 'screening-phase-id', + type: 'SCREENING', }, { id: 'approval-scorecard-id', name: 'Approval Scorecard', - phaseId: 'approval-phase-id', + type: 'APPROVAL', }, ]) 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 f0be18574..a34561357 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 @@ -546,16 +546,26 @@ function getFallbackReviewerPhaseId( || getChallengePhaseId(phases[0]) } +/** + * Resolves the challenge phase targeted by a default reviewer row. + * + * @param defaultReviewer default reviewer configuration returned by the challenge API. + * @param phases current challenge phases used to validate ids and resolve phase-name-only defaults. + * @returns the matching challenge phase template id, or a selectable fallback when no match exists. + * @remarks Timeline-specific reviewer defaults commonly provide `phaseName` with a null `phaseId`, + * so the name must be resolved before using the legacy fallback. + * @throws Does not throw. + */ function getReviewerPhaseId( defaultReviewer: DefaultReviewer | undefined, phases: ChallengeEditorFormData['phases'], ): string | undefined { + const phaseRows = Array.isArray(phases) + ? phases + : [] const defaultPhaseId = normalizeText(defaultReviewer?.phaseId) if (defaultPhaseId) { - const phaseRows = Array.isArray(phases) - ? phases - : [] const hasMatchingPhase = !phaseRows.length || phaseRows.some(phase => getChallengePhaseId(phase) === defaultPhaseId) @@ -564,6 +574,18 @@ function getReviewerPhaseId( } } + const defaultPhaseName = normalizeKey(defaultReviewer?.phaseName) + if (defaultPhaseName) { + const matchingPhase = phaseRows.find(phase => ( + normalizeKey(phase?.name) === defaultPhaseName + )) + const matchingPhaseId = getChallengePhaseId(matchingPhase) + + if (matchingPhaseId) { + return matchingPhaseId + } + } + return getFallbackReviewerPhaseId(phases) }