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
69 changes: 69 additions & 0 deletions src/apps/work/src/lib/utils/challenge-editor.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,75 @@ describe('challenge-editor utils schedule mapping', () => {
scheduledStartDate: '2026-04-09T15:05:00.000Z',
}])
})

it('persists a legacy non-task schedule when its saved scheduling flag is false', () => {
const formData: Record<string, unknown> = {
description: 'Public specification',
legacy: {
isTask: false,
useSchedulingAPI: false,
},
name: 'Legacy scheduled challenge',
phases: [{
duration: 7200,
id: 'challenge-phase-1',
phaseId: 'submission-phase',
scheduledEndDate: '2026-07-25T04:21:00.737Z',
scheduledStartDate: '2026-07-20T04:21:00.737Z',
}],
skills: [],
startDate: '2026-07-20T04:21:00.737Z',
tags: [],
trackId: 'track-id',
typeId: 'type-id',
}

const result = transformFormDataToChallenge(formData as any)

expect(result.legacy?.useSchedulingAPI)
.toBe(true)
expect(result.startDate)
.toBe('2026-07-20T04:21:00.737Z')
expect(result.phases)
.toEqual([expect.objectContaining({
id: 'challenge-phase-1',
scheduledEndDate: '2026-07-25T04:21:00.737Z',
scheduledStartDate: '2026-07-20T04:21:00.737Z',
})])
})

it('does not enable scheduling for a task with a legacy disabled flag', () => {
const formData: Record<string, unknown> = {
description: 'Task specification',
legacy: {
isTask: true,
useSchedulingAPI: false,
},
name: 'Legacy task',
phases: [{
duration: 60,
phaseId: 'submission-phase',
scheduledEndDate: '2026-07-20T05:21:00.737Z',
scheduledStartDate: '2026-07-20T04:21:00.737Z',
}],
skills: [],
startDate: '2026-07-20T04:21:00.737Z',
tags: [],
trackId: 'track-id',
typeId: 'task-type-id',
}

const result = transformFormDataToChallenge(formData as any)

expect(result.legacy?.useSchedulingAPI)
.toBe(false)
expect(result)
.not
.toHaveProperty('startDate')
expect(result)
.not
.toHaveProperty('phases')
})
})

describe('challenge-editor utils task reviewer mapping', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/apps/work/src/lib/utils/challenge-editor.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,11 @@ export function transformFormDataToChallenge(
id: skill.id,
name: skill.name,
}))
const hasEditableSchedule = formData.legacy?.isTask !== true
&& Array.isArray(formData.phases)
&& formData.phases.length > 0
const isSchedulingEnabled = isSchedulingApiEnabled(formData.legacy?.useSchedulingAPI)
|| hasEditableSchedule
const metadataWithoutMilestone = normalizeMetadataEntries(formData.metadata)
.filter(metadataEntry => !MILESTONE_METADATA_KEYS.includes(metadataEntry.name))
const milestoneMetadata = buildMilestoneMetadata(formData.milestoneConfiguration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ The form uses `challengeBasicInfoSchema` from `src/apps/work/src/lib/schemas/cha
Engineering, and other active API-only/internal challenge types stay hidden from the create
dropdown, and any now-invalid preselection is cleared when the track changes. Deployments can
override the allowlist with `REACT_APP_WORK_CREATE_CHALLENGE_TYPES_BY_TRACK` JSON.
- `ChallengeScheduleSection`: schedule editor for challenge start and phase dates. It keeps the detected timezone above the controls, renders the `Start Date` label with the `Scheduled` and `Immediately` start-mode radios aligned to the end of that header row above the input with a green selected state, keeps outside-label date-picker controls visible and interactive when the shared input wrapper omits an empty internal label, persists the selected start mode in challenge metadata so saved `/edit` and `/view` routes reopen with the correct radio state, initializes missing challenge start dates from existing phase starts or the current date before calculating blank phase rows, recalculates root phase dates when the challenge start changes, honors completed phases' actual dates when deriving and displaying schedule rows, lets incomplete active Design phases be shortened no earlier than the current date/time, prevents incomplete active non-Design phases from being shortened, and keeps completed phases' end-date and duration controls locked to match legacy work-manager behavior. `Task` challenges hide this editable section across create, edit, and read-only view routes to match legacy work-manager behavior.
- `ChallengeScheduleSection`: schedule editor for challenge start and phase dates. It keeps the detected timezone above the controls, renders the `Start Date` label with the `Scheduled` and `Immediately` start-mode radios aligned to the end of that header row above the input with a green selected state, keeps outside-label date-picker controls visible and interactive when the shared input wrapper omits an empty internal label, persists the selected start mode in challenge metadata so saved `/edit` and `/view` routes reopen with the correct radio state, initializes missing challenge start dates from existing phase starts or the current date before calculating blank phase rows, recalculates root phase dates when the challenge start changes, and upgrades populated legacy non-task schedules to the scheduling API during serialization even if asynchronous hydration restores a stale disabled flag. It honors completed phases' actual dates when deriving and displaying schedule rows, lets incomplete active Design phases be shortened no earlier than the current date/time, prevents incomplete active non-Design phases from being shortened, and keeps completed phases' end-date and duration controls locked to match legacy work-manager behavior. `Task` challenges hide this editable section across create, edit, and read-only view routes to match legacy work-manager behavior and retain a disabled legacy scheduling flag.
- `DesignWorkTypeField`: shown for Design + Challenge, with the legacy work-type options (`Application Front-End Design`, `Print/Presentation`, `Web Design`, `Widget or Mobile Screen Design`, `Wireframes`). The selected value is stored in challenge tags.
- `FunChallengeField`: shown for `Marathon Match` type and remains editable after creation so the form can switch between fun-challenge and standard marathon-match fields.
- `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.
Expand Down
Loading