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 @@ -179,6 +179,7 @@ jest.mock('./components', () => {

return {
ChallengeEditorForm: (props: {
isEditMode?: boolean
onChallengeCreated?: (challenge: {
id: string
name: string
Expand All @@ -201,7 +202,12 @@ jest.mock('./components', () => {
}

return (
<div>
<div
data-edit-mode={props.isEditMode
? 'true'
: 'false'}
data-testid='challenge-editor-form'
>
{props.isReadOnly
? 'Challenge View Form'
: 'Challenge Editor Form'}
Expand Down Expand Up @@ -302,6 +308,11 @@ describe('ChallengeEditorPage', () => {
.toBeTruthy()
})

expect(
screen.getByTestId('challenge-editor-form')
.getAttribute('data-edit-mode'),
)
.toBe('true')
expect(screen.getByRole('button', { name: 'Cancel' }))
.toBeTruthy()

Expand Down Expand Up @@ -357,6 +368,11 @@ describe('ChallengeEditorPage', () => {
.toBeTruthy()
})

expect(
screen.getByTestId('challenge-editor-form')
.getAttribute('data-edit-mode'),
)
.toBe('false')
expect(screen.getByRole('heading', { name: 'View Edit test' }))
.toBeTruthy()
expect(screen.getByRole('button', { name: 'Cancel' }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,13 +791,15 @@ const EditorTabs: FC<EditorTabsProps> = (props: EditorTabsProps) => (
const ChallengeEditorContent: FC<ChallengeEditorContentProps> = (
props: ChallengeEditorContentProps,
) => {
const isEditMode = props.isExistingChallenge && !props.isReadOnly
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

if (!props.isExistingChallenge || props.activeTab === 'details') {
return (
<ChallengeEditorForm
canLaunchChallenge={props.canLaunchChallenge}
challenge={props.challenge}
isLaunchDisabled={props.isLaunchDisabled}
isEditMode={props.isExistingChallenge}
isEditMode={isEditMode}
isReadOnly={props.isReadOnly}
launchButtonLabel={props.launchButtonLabel}
onChallengeCreated={props.onChallengeCreated}
Expand Down Expand Up @@ -833,7 +835,7 @@ const ChallengeEditorContent: FC<ChallengeEditorContentProps> = (
canLaunchChallenge={props.canLaunchChallenge}
challenge={props.challenge}
isLaunchDisabled={props.isLaunchDisabled}
isEditMode={props.isExistingChallenge}
isEditMode={isEditMode}
isReadOnly={props.isReadOnly}
launchButtonLabel={props.launchButtonLabel}
onChallengeCreated={props.onChallengeCreated}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The form uses `challengeBasicInfoSchema` from `src/apps/work/src/lib/schemas/cha
- `ChallengeNameField`: text input.
- `ChallengeTrackField`: track selector from `useFetchChallengeTracks`.
- `ChallengeTypeField`: active type selector from `useFetchChallengeTypes`, excluding `Topgear Task` because that flow is not launchable from the work app editor. When the selected track is Design or QA, it also hides `Marathon Match` to match the legacy work-manager create flow and clears any now-invalid preselection.
- `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, and recalculates root phase dates when the challenge start changes.
- `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, and recalculates root phase dates when the challenge start changes. Existing `Task` challenges hide this editable section only on the edit route, while create and read-only view routes still show the schedule.
- `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 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
ChallengeEditorForm,
getTaskLaunchValidationError,
} from './ChallengeEditorForm'
import { TermsField } from './TermsField'

jest.mock('../../../../lib/components/form', () => ({
FormCheckboxField: () => <></>,
Expand Down Expand Up @@ -191,7 +192,7 @@ jest.mock('~/config', () => ({
virtual: true,
})
jest.mock('./AssignedMemberField', () => ({
AssignedMemberField: () => <></>,
AssignedMemberField: () => <span>Assigned Member Field</span>,
}))
jest.mock('./AttachmentsField', () => {
const reactHookForm: typeof import('react-hook-form') = jest.requireActual('react-hook-form')
Expand Down Expand Up @@ -433,7 +434,7 @@ jest.mock('./ReviewersField', () => ({
ReviewersField: () => <></>,
}))
jest.mock('./ReviewTypeField', () => ({
ReviewTypeField: () => <></>,
ReviewTypeField: () => <span>Review Type Field</span>,
}))
jest.mock('./RoundTypeField', () => ({
RoundTypeField: () => <></>,
Expand All @@ -445,7 +446,7 @@ jest.mock('./SubmissionVisibilityField', () => ({
SubmissionVisibilityField: () => <>Submission Visibility Field</>,
}))
jest.mock('./TermsField', () => ({
TermsField: () => <></>,
TermsField: jest.fn(() => <></>),
}))

const mockedUseAutosave = useAutosave as jest.Mock
Expand All @@ -465,6 +466,7 @@ const mockedFetchResourceRolesService = fetchResourceRoles as jest.Mock
const mockedFetchResourcesService = fetchResources as jest.Mock
const mockedShowErrorToast = showErrorToast as jest.Mock
const mockedShowSuccessToast = showSuccessToast as jest.Mock
const mockedTermsField = TermsField as jest.MockedFunction<typeof TermsField>

describe('ChallengeEditorForm', () => {
const draftChallenge = {
Expand All @@ -491,6 +493,9 @@ describe('ChallengeEditorForm', () => {
} as Challenge
const taskDraftChallenge = {
...draftChallenge,
task: {
isTask: true,
},
type: {
abbreviation: 'TSK',
name: 'Task',
Expand Down Expand Up @@ -690,6 +695,58 @@ describe('ChallengeEditorForm', () => {
.toBeInTheDocument()
})

it('keeps the task timeline hidden in edit mode when only the persisted task flag is available', () => {
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [],
isLoading: false,
})

render(
<MemoryRouter>
<ChallengeEditorForm
challenge={{
...draftChallenge,
task: {
isTask: true,
},
typeId: 'task-type-id',
}}
isEditMode
/>
</MemoryRouter>,
)

expect(screen.queryByRole('heading', { name: 'Timeline & Schedule' }))
.toBeNull()
})

it('keeps task-only controls visible in edit mode when only the persisted task flag is available', () => {
mockedUseFetchChallengeTypes.mockReturnValue({
challengeTypes: [],
isLoading: false,
})

render(
<MemoryRouter>
<ChallengeEditorForm
challenge={{
...draftChallenge,
task: {
isTask: true,
},
typeId: 'task-type-id',
}}
isEditMode
/>
</MemoryRouter>,
)

expect(screen.getByText('Assigned Member Field'))
.toBeInTheDocument()
expect(screen.getByText('Review Type Field'))
.toBeInTheDocument()
})

it('renders secondary footer actions and a primary launch action for draft challenges', () => {
render(
<MemoryRouter>
Expand Down Expand Up @@ -761,6 +818,39 @@ describe('ChallengeEditorForm', () => {
}))
})

it('does not default the standard term when viewing an existing challenge', () => {
render(
<MemoryRouter>
<ChallengeEditorForm
challenge={draftChallenge}
isReadOnly
/>
</MemoryRouter>,
)

expect(mockedTermsField)
.toHaveBeenCalled()
expect(mockedTermsField.mock.calls[mockedTermsField.mock.calls.length - 1][0])
.toEqual(expect.objectContaining({
shouldDefaultStandardTerm: false,
}))
})

it('defaults the standard term for created challenges outside edit and view mode', () => {
render(
<MemoryRouter>
<ChallengeEditorForm challenge={draftChallenge} />
</MemoryRouter>,
)

expect(mockedTermsField)
.toHaveBeenCalled()
expect(mockedTermsField.mock.calls[mockedTermsField.mock.calls.length - 1][0])
.toEqual(expect.objectContaining({
shouldDefaultStandardTerm: true,
}))
})

it('preserves project billing markup when fetched draft data resets the form', async () => {
mockedUseFetchProjectBillingAccount.mockReturnValue({
billingAccount: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,13 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
selectedChallengeType,
],
)
const persistedTaskFlag = useMemo(
(): boolean => props.challenge?.task?.isTask === true || values.legacy?.isTask === true,
[
props.challenge?.task?.isTask,
values.legacy?.isTask,
],
)
const hasResolvedChallengeType = useMemo(
(): boolean => !!normalizeTextValue(resolvedChallengeTypeName)
|| !!normalizeTextValue(resolvedChallengeTypeAbbreviation),
Expand All @@ -1148,6 +1155,18 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
resolvedChallengeTypeName,
],
)
const isTaskChallenge = useMemo(
(): boolean => shouldTreatChallengeAsTask({
hasResolvedChallengeType,
isTaskTypeSelected: isTaskChallengeSelected,
persistedTaskFlag,
}),
[
hasResolvedChallengeType,
isTaskChallengeSelected,
persistedTaskFlag,
],
)
const isMarathonMatchChallengeSelected = useMemo(
(): boolean => isMarathonMatchChallengeTypeByNameAndAbbreviation({
abbreviation: resolvedChallengeTypeAbbreviation,
Expand Down Expand Up @@ -1204,15 +1223,15 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
const showFunChallengeField = isMarathonMatchChallengeSelected
const showMarathonMatchScorerSection = isMarathonMatchChallengeSelected && isChallengeCreated
const showPrizesAndBillingSection = !isFunChallengeSelected
const showEditableTimelineSection = !isEditMode || !isTaskChallengeSelected
const showEditableTimelineSection = !isEditMode || !isTaskChallenge
const usesManualReviewers = useMemo(
(): boolean => shouldUseManualReviewers({
isMarathonMatchChallenge: isMarathonMatchChallengeSelected,
isTaskChallenge: isTaskChallengeSelected,
isTaskChallenge,
}),
[
isMarathonMatchChallengeSelected,
isTaskChallengeSelected,
isTaskChallenge,
],
)
const isScorerBlockingChallengeActions = showMarathonMatchScorerSection
Expand All @@ -1238,10 +1257,11 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
): boolean => shouldTreatChallengeAsTask({
hasResolvedChallengeType,
isTaskTypeSelected: isTaskChallengeSelected,
persistedTaskFlag: formData.legacy?.isTask === true,
persistedTaskFlag: props.challenge?.task?.isTask === true || formData.legacy?.isTask === true,
}), [
hasResolvedChallengeType,
isTaskChallengeSelected,
props.challenge?.task?.isTask,
])
const applyPersistedSingleAssignments = useCallback((
formData: ChallengeEditorFormData,
Expand Down Expand Up @@ -1655,12 +1675,12 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
])

useEffect(() => {
setValue('legacy.isTask', isTaskChallengeSelected, {
setValue('legacy.isTask', isTaskChallenge, {
shouldDirty: false,
shouldValidate: true,
})
}, [
isTaskChallengeSelected,
isTaskChallenge,
setValue,
])

Expand Down Expand Up @@ -1938,14 +1958,14 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
isSaveAsDraft,
payloadStatus,
}: SaveStatusMetadata = getSaveStatusMetadata(formData.status, options)
const shouldTreatSaveAsTaskChallenge = isTaskSingleAssignmentChallenge(formData)
const currentStatus = normalizeStatus(formData.status)
const isChallengeBeingActivated = payloadStatus === CHALLENGE_STATUS.ACTIVE
&& currentStatus !== CHALLENGE_STATUS.ACTIVE
const isTaskChallenge = isTaskSingleAssignmentChallenge(formData)
const taskLaunchValidationError = getTaskLaunchValidationError({
assignedMemberId: formData.assignedMemberId,
currentStatus: formData.status,
isTaskChallenge,
isTaskChallenge: shouldTreatSaveAsTaskChallenge,
nextStatus: payloadStatus,
})

Expand Down Expand Up @@ -2021,7 +2041,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
},
),
formDataWithProjectBilling,
isTaskChallenge,
shouldTreatSaveAsTaskChallenge,
)
const savedAt = new Date()

Expand Down Expand Up @@ -2164,7 +2184,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
const reviewerValidationError = getReviewerValidationError(formData, {
challengeTypeAbbreviation: resolvedChallengeTypeAbbreviation,
challengeTypeName: resolvedChallengeTypeName,
isTaskChallenge: isTaskChallengeSelected,
isTaskChallenge,
requiredReviewersErrorMessage:
'Reviewers are required for configured review phases before saving as draft.',
})
Expand All @@ -2187,7 +2207,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
[
clearErrors,
isScorerBlockingChallengeActions,
isTaskChallengeSelected,
isTaskChallenge,
resolvedChallengeTypeAbbreviation,
resolvedChallengeTypeName,
saveChallenge,
Expand Down Expand Up @@ -2383,18 +2403,18 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Advanced Options</h3>
<div className={styles.grid}>
{isTaskChallengeSelected
{isTaskChallenge
? <AssignedMemberField />
: undefined}
{isTaskChallengeSelected
{isTaskChallenge
? (
<ReviewTypeField
isTaskChallenge={isTaskChallengeSelected}
isTaskChallenge={isTaskChallenge}
/>
)
: undefined}
<GroupsField />
<TermsField shouldDefaultStandardTerm={!isEditMode} />
<TermsField shouldDefaultStandardTerm={!isEditMode && !isReadOnly} />
<NDAField />
<FormCheckboxField
checkboxOnlyHitArea
Expand Down
Loading