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 @@ -23,7 +23,7 @@
payload is available, so the create route can expand to the full editor immediately after the
initial draft is created.
- `components/*Field.tsx`: field-level components for each challenge section.
- `components/ReviewersField/*`: tabbed human/AI review configuration. Human reviewers stay on the challenge form, while AI reviewer configs load/save through the review API and sync saved AI workflows back into the challenge `reviewers` array. Existing AI configs are reloaded once per saved challenge even if the challenge payload is temporarily missing synced AI reviewer rows, while still avoiding empty-config lookups for unsaved challenges, ordinary parent rerenders in edit mode, and same-session re-fetches right after a config is intentionally removed. Removing an AI config also detaches the synced AI workflow reviewers from the challenge. In read-only view mode the tab switcher remains clickable so users can inspect AI config details inside the disabled challenge form, and the review summary surfaces the human-review table, AI workflow details, resolved scorecard names, review flow, and estimated reviewer cost without requiring edits. Repeated human-review rows that share the same resource role now consume persisted challenge-resource assignments in row order so every assigned reviewer still appears once in the summary, and mixed legacy resource layouts continue into the generic `Reviewer` fallback pool when a phase-specific role runs out of persisted assignments. The editor hydration, editable tab, summary, and post-save reset now tolerate persisted resource rows that only expose role names, member handles, or member ids instead of the full modern payload shape, so refreshed drafts and newly saved drafts reopen with the saved reviewer assignments intact. Initial persisted-resource hydration also keeps running while the form is still in its mount-time normalization window, so internal dirty flags from compatibility fields do not block restored copilot or reviewer assignments after a full refresh. The AI-gating failure path keeps the locked state grouped under the gate so the diagram matches the legacy work-manager layout, including `AI_GATING` configs whose workflows do not explicitly mark `isGating`. On narrow screens the review-flow diagram switches to a compact portrait branch: submission stays full width, the `AI Gate` and `Locked` states sit side by side as narrower cards, the `< threshold` connector sits between those two cards, and the human-review path continues only from the gate column. When AI reviewers exist without a persisted AI screening phase, the schedule editor injects a virtual `AI Screening` row after submission phases. This `Review` section is hidden for `Task` and `Marathon Match` challenges because those flows use dedicated reviewer assignment UIs.
- `components/ReviewersField/*`: tabbed human/AI review configuration. Copilot-only users editing Design `Challenge` types see one Screener selector that synchronizes the selected member across final Screening and, for two-round challenges, Checkpoint Screening while preserving all hidden phase, scorecard, and reviewer defaults; admins and managers retain the full interface. Human reviewers stay on the challenge form, while AI reviewer configs load/save through the review API and sync saved AI workflows back into the challenge `reviewers` array. Existing AI configs are reloaded once per saved challenge even if the challenge payload is temporarily missing synced AI reviewer rows, while still avoiding empty-config lookups for unsaved challenges, ordinary parent rerenders in edit mode, and same-session re-fetches right after a config is intentionally removed. Removing an AI config also detaches the synced AI workflow reviewers from the challenge. In read-only view mode the tab switcher remains clickable so users can inspect AI config details inside the disabled challenge form, and the review summary surfaces the human-review table, AI workflow details, resolved scorecard names, review flow, and estimated reviewer cost without requiring edits. Repeated human-review rows that share the same resource role now consume persisted challenge-resource assignments in row order so every assigned reviewer still appears once in the summary, and mixed legacy resource layouts continue into the generic `Reviewer` fallback pool when a phase-specific role runs out of persisted assignments. The editor hydration, editable tab, summary, and post-save reset now tolerate persisted resource rows that only expose role names, member handles, or member ids instead of the full modern payload shape, so refreshed drafts and newly saved drafts reopen with the saved reviewer assignments intact. Initial persisted-resource hydration also keeps running while the form is still in its mount-time normalization window, so internal dirty flags from compatibility fields do not block restored copilot or reviewer assignments after a full refresh. The AI-gating failure path keeps the locked state grouped under the gate so the diagram matches the legacy work-manager layout, including `AI_GATING` configs whose workflows do not explicitly mark `isGating`. On narrow screens the review-flow diagram switches to a compact portrait branch: submission stays full width, the `AI Gate` and `Locked` states sit side by side as narrower cards, the `< threshold` connector sits between those two cards, and the human-review path continues only from the gate column. When AI reviewers exist without a persisted AI screening phase, the schedule editor injects a virtual `AI Screening` row after submission phases. This `Review` section is hidden for `Task` and `Marathon Match` challenges because those flows use dedicated reviewer assignment UIs.
- `ChallengeEditorPage.module.scss` and `components/ChallengeEditorForm.module.scss`: page and form layout styling, including the grouped `Prizes & Billing` layout that keeps the challenge-prizes and copilot-fee inputs at fixed widths on larger screens, preserves whitespace to the right, and moves the billing summary underneath them.

## Validation Rules
Expand Down Expand Up @@ -80,7 +80,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. On the 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. Design challenge manual reviewers always keep the public review opportunity checkbox disabled and unchecked. Standard Screening member selectors remain available but are optional so a copilot can assign the Screener after launch.
- `ReviewersField`: hidden for `Task` and `Marathon Match` challenges because manual reviewer assignment is handled elsewhere. Copilot-only Design Challenge editing exposes only the shared Screener selector; selecting, replacing, or clearing it updates both the `Screener` and any `Checkpoint Screener` challenge resources. 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. Design challenge manual reviewers always keep the public review opportunity checkbox disabled and unchecked. Standard Screening member selectors remain available but are optional so a copilot can assign the 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 @@ -684,7 +684,10 @@ jest.mock('./ReviewCostField', () => ({
ReviewCostField: () => <></>,
}))
jest.mock('./ReviewersField', () => ({
ReviewersField: (props: { isReadOnly?: boolean }) => {
ReviewersField: (props: {
isReadOnly?: boolean
screenerOnly?: boolean
}) => {
const reactHookForm: typeof import('react-hook-form') = jest.requireActual('react-hook-form')
const reviewers = reactHookForm.useWatch({
control: reactHookForm.useFormContext().control,
Expand All @@ -695,6 +698,7 @@ jest.mock('./ReviewersField', () => ({
<div
data-read-only={props.isReadOnly === true ? 'true' : 'false'}
data-reviewers={JSON.stringify(reviewers || [])}
data-screener-only={props.screenerOnly === true ? 'true' : 'false'}
data-testid='reviewers-field'
>
Reviewers Field
Expand Down Expand Up @@ -3058,6 +3062,131 @@ describe('ChallengeEditorForm', () => {
.toBeNull()
})

it('uses the simplified screener review for a copilot editing a Design Challenge', () => {
mockedUseFetchChallengeTracks.mockReturnValue({
isLoading: false,
tracks: [{
id: 'design-track-id',
name: 'Design',
track: 'DESIGN',
}],
})
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [{
abbreviation: 'CH',
id: 'design-challenge-type-id',
name: 'Challenge',
}],
isLoading: false,
})

render(
<MemoryRouter>
<WorkAppContext.Provider value={copilotContextValue}>
<ChallengeEditorForm challenge={designChallengeWithDeferredScreener} />
</WorkAppContext.Provider>
</MemoryRouter>,
)

expect(screen.getByTestId('reviewers-field'))
.toHaveAttribute('data-screener-only', 'true')
})

it('uses persisted Design Challenge metadata for the copilot review gate', () => {
mockedUseFetchChallengeTracks.mockReturnValue({
isLoading: false,
tracks: [],
})
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [],
isLoading: false,
})

render(
<MemoryRouter>
<WorkAppContext.Provider value={copilotContextValue}>
<ChallengeEditorForm
challenge={{
...designChallengeWithDeferredScreener,
track: {
abbreviation: 'DESIGN',
name: 'Design',
},
}}
/>
</WorkAppContext.Provider>
</MemoryRouter>,
)

expect(screen.getByTestId('reviewers-field'))
.toHaveAttribute('data-screener-only', 'true')
})

it('keeps the full review configuration for an admin editing a Design Challenge', () => {
mockedUseFetchChallengeTracks.mockReturnValue({
isLoading: false,
tracks: [{
id: 'design-track-id',
name: 'Design',
track: 'DESIGN',
}],
})
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [{
abbreviation: 'CH',
id: 'design-challenge-type-id',
name: 'Challenge',
}],
isLoading: false,
})
const adminContextValue: WorkAppContextModel = {
...copilotContextValue,
isAdmin: true,
userRoles: ['administrator'],
}

render(
<MemoryRouter>
<WorkAppContext.Provider value={adminContextValue}>
<ChallengeEditorForm challenge={designChallengeWithDeferredScreener} />
</WorkAppContext.Provider>
</MemoryRouter>,
)

expect(screen.getByTestId('reviewers-field'))
.toHaveAttribute('data-screener-only', 'false')
})

it('keeps the full review configuration for a copilot editing a Design First2Finish', () => {
mockedUseFetchChallengeTracks.mockReturnValue({
isLoading: false,
tracks: [{
id: 'design-track',
name: 'Design',
track: 'DESIGN',
}],
})
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [{
abbreviation: 'F2F',
id: 'design-first2finish',
name: 'First2Finish',
}],
isLoading: false,
})

render(
<MemoryRouter>
<WorkAppContext.Provider value={copilotContextValue}>
<ChallengeEditorForm challenge={first2FinishDraftChallenge} />
</WorkAppContext.Provider>
</MemoryRouter>,
)

expect(screen.getByTestId('reviewers-field'))
.toHaveAttribute('data-screener-only', 'false')
})

it('does not delete manual iterative reviewer resources when saving a first2finish draft', async () => {
const user = userEvent.setup()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2017,17 +2017,17 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
)
const isChallengeTypeSelected = useMemo(
(): boolean => {
const normalizedChallengeTypeName = (selectedChallengeType?.name || '')
const normalizedChallengeTypeName = (resolvedChallengeTypeName || '')
.trim()
.toUpperCase()
const normalizedChallengeTypeAbbreviation = (selectedChallengeType?.abbreviation || '')
const normalizedChallengeTypeAbbreviation = (resolvedChallengeTypeAbbreviation || '')
.trim()
.toUpperCase()

return normalizedChallengeTypeName === CHALLENGE_TYPE_CHALLENGE_NAME
|| normalizedChallengeTypeAbbreviation === CHALLENGE_TYPE_CHALLENGE_ABBREVIATION
},
[selectedChallengeType],
[resolvedChallengeTypeAbbreviation, resolvedChallengeTypeName],
)
const isTaskChallengeSelected = useMemo(
(): boolean => isTaskChallengeType(selectedChallengeType)
Expand Down Expand Up @@ -2219,6 +2219,9 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
const shouldUseCopilotBillingSummary = workAppContext.isCopilot
&& !workAppContext.isAdmin
&& !workAppContext.isManager
const shouldUseSimplifiedDesignReview = shouldUseCopilotBillingSummary
&& isDesignTrackSelected
&& isChallengeTypeSelected
const getPersistedAssignmentValueByFields = useCallback((
fallbackValue: string | undefined,
roleNames: readonly string[],
Expand Down Expand Up @@ -3838,6 +3841,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
onConfigSaveControllerReady={function (controller: AiReviewConfigSaveController | undefined) {
aiReviewConfigSaveControllerRef.current = controller
}}
screenerOnly={shouldUseSimplifiedDesignReview}
/>
</div>
</section>
Expand Down
Loading
Loading