From bff7a2c930dba9e1344aff57055dadb99a94e82d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 15 Jul 2026 16:36:28 +1000 Subject: [PATCH] Fix for timeline issue noted in prod --- .../lib/utils/challenge-editor.utils.spec.ts | 69 +++++++++++++++++++ .../src/lib/utils/challenge-editor.utils.ts | 4 ++ .../challenges/ChallengeEditorPage/README.md | 2 +- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/apps/work/src/lib/utils/challenge-editor.utils.spec.ts b/src/apps/work/src/lib/utils/challenge-editor.utils.spec.ts index 42197d6de..b0e556d66 100644 --- a/src/apps/work/src/lib/utils/challenge-editor.utils.spec.ts +++ b/src/apps/work/src/lib/utils/challenge-editor.utils.spec.ts @@ -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 = { + 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 = { + 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', () => { diff --git a/src/apps/work/src/lib/utils/challenge-editor.utils.ts b/src/apps/work/src/lib/utils/challenge-editor.utils.ts index db694ab75..0ed5b02de 100644 --- a/src/apps/work/src/lib/utils/challenge-editor.utils.ts +++ b/src/apps/work/src/lib/utils/challenge-editor.utils.ts @@ -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) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md index fd993d139..596a5c288 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md @@ -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.