Skip to content

Commit e642af9

Browse files
authored
Merge pull request #1632 from topcoder-platform/PM-4702
PM-4702: return challenge saves to view mode
2 parents aa515b5 + 998fa89 commit e642af9

3 files changed

Lines changed: 96 additions & 5 deletions

File tree

src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
an `Edit` action only when the challenge is not completed.
1111
- `components/ChallengeEditorForm.tsx`: React Hook Form container with autosave and manual save.
1212
In view mode it renders the existing challenge data in a disabled fieldset and omits save/launch
13-
footer actions.
13+
footer actions. Manual saves from an existing `/edit` route navigate back to the matching `/view`
14+
route after the update succeeds.
1415
- `components/*Field.tsx`: field-level components for each challenge section.
1516
- `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 only when the challenge already has synced AI reviewer entries or the challenge changes, which avoids empty-config lookups on new challenges and prevents ordinary parent rerenders from refetching the same config in edit mode. Removing an AI config also detaches the synced AI workflow reviewers from the challenge. 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.
1617
- `ChallengeEditorPage.module.scss` and `components/ChallengeEditorForm.module.scss`: page and form layout styling, including the grouped `Prizes & Billing` layout that keeps the editable inputs together at fixed widths on larger screens and moves the billing summary underneath them.

src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
} from '@testing-library/react'
99
import '@testing-library/jest-dom'
1010
import userEvent from '@testing-library/user-event'
11-
import { MemoryRouter } from 'react-router-dom'
11+
import {
12+
MemoryRouter,
13+
useLocation,
14+
} from 'react-router-dom'
1215

1316
import {
1417
useAutosave,
@@ -466,6 +469,12 @@ const mockedFetchResourcesService = fetchResources as jest.Mock
466469
const mockedShowErrorToast = showErrorToast as jest.Mock
467470
const mockedShowSuccessToast = showSuccessToast as jest.Mock
468471

472+
const LocationDisplay = (): JSX.Element => {
473+
const location = useLocation()
474+
475+
return <div data-testid='location-display'>{location.pathname}</div>
476+
}
477+
469478
describe('ChallengeEditorForm', () => {
470479
const draftChallenge = {
471480
id: '12345',
@@ -1380,6 +1389,32 @@ describe('ChallengeEditorForm', () => {
13801389
})
13811390
})
13821391

1392+
it('returns to view mode after saving from an edit route', async () => {
1393+
const user = userEvent.setup()
1394+
1395+
mockedPatchChallenge.mockResolvedValue(validDraftChallenge)
1396+
1397+
render(
1398+
<MemoryRouter initialEntries={['/projects/100578/challenges/12345/edit']}>
1399+
<LocationDisplay />
1400+
<ChallengeEditorForm
1401+
challenge={validDraftChallenge}
1402+
projectId='100578'
1403+
/>
1404+
</MemoryRouter>,
1405+
)
1406+
1407+
await user.type(screen.getByLabelText('Challenge Name'), ' updated')
1408+
await user.click(screen.getByRole('button', { name: 'Save Challenge' }))
1409+
1410+
await waitFor(() => {
1411+
expect(mockedPatchChallenge)
1412+
.toHaveBeenCalledTimes(1)
1413+
expect(screen.getByTestId('location-display'))
1414+
.toHaveTextContent('/projects/100578/challenges/12345/view')
1415+
})
1416+
})
1417+
13831418
it('clears Marathon Match when a new challenge switches to the Design track', async () => {
13841419
const user = userEvent.setup()
13851420

src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { FormProvider, useForm } from 'react-hook-form'
1010
import {
1111
Link,
12+
useLocation,
1213
useNavigate,
1314
} from 'react-router-dom'
1415

@@ -197,6 +198,7 @@ interface ChallengeEditorFormProps {
197198

198199
interface SaveChallengeOptions {
199200
isAutosave?: boolean
201+
redirectToViewOnSuccess?: boolean
200202
statusOverride?: string
201203
successMessage?: string
202204
}
@@ -206,6 +208,14 @@ interface SaveStatusMetadata {
206208
payloadStatus?: string
207209
}
208210

211+
interface ResolvePostSaveNavigationPathParams {
212+
isEditMode?: boolean
213+
isSaveAsDraft: boolean
214+
redirectToViewOnSuccess?: boolean
215+
savedChallengeId: string
216+
viewModePath?: string
217+
}
218+
209219
type SingleAssignmentFieldName = 'assignedMemberId' | 'copilot' | 'reviewer'
210220

211221
interface SingleAssignmentConfig {
@@ -945,6 +955,29 @@ export function getTaskLaunchValidationError(
945955
return TASK_ASSIGNED_MEMBER_REQUIRED_FOR_LAUNCH_MESSAGE
946956
}
947957

958+
/**
959+
* Resolves the next route after a manual challenge save succeeds.
960+
*
961+
* New draft saves still enter edit mode so the full form becomes available. Existing edit-route
962+
* saves return to the matching read-only challenge route when requested by the caller.
963+
*
964+
* @param params Save-context values needed to choose the next route.
965+
* @returns The post-save route, or `undefined` when the user should stay on the current page.
966+
*/
967+
function resolvePostSaveNavigationPath(
968+
params: ResolvePostSaveNavigationPathParams,
969+
): string | undefined {
970+
if (params.isSaveAsDraft && !params.isEditMode) {
971+
return `/challenges/${encodeURIComponent(params.savedChallengeId)}/edit`
972+
}
973+
974+
if (params.redirectToViewOnSuccess && params.viewModePath) {
975+
return params.viewModePath
976+
}
977+
978+
return undefined
979+
}
980+
948981
/**
949982
* Creates an error for launch-blocking validation paths that already surfaced a
950983
* specific message to the user.
@@ -983,6 +1016,7 @@ function isHandledLaunchBlockError(
9831016
export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
9841017
props: ChallengeEditorFormProps,
9851018
) => {
1019+
const location = useLocation()
9861020
const navigate = useNavigate()
9871021
const isEditMode = props.isEditMode
9881022
const isReadOnly = props.isReadOnly === true
@@ -1001,6 +1035,16 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
10011035
props.projectId,
10021036
],
10031037
)
1038+
const viewModePath = useMemo(
1039+
(): string | undefined => {
1040+
if (!location.pathname.endsWith('/edit')) {
1041+
return undefined
1042+
}
1043+
1044+
return `${location.pathname.slice(0, -'/edit'.length)}/view`
1045+
},
1046+
[location.pathname],
1047+
)
10041048
const projectBillingAccountResult = useFetchProjectBillingAccount(fallbackProjectId)
10051049
const projectBillingAccount = projectBillingAccountResult.billingAccount
10061050
const projectBillingAccountRef = useRef(projectBillingAccount)
@@ -2036,8 +2080,16 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
20362080
showSuccessToast(getSaveSuccessMessage(isSaveAsDraft, options))
20372081
}
20382082

2039-
if (isSaveAsDraft && !isEditMode) {
2040-
navigate(`/challenges/${encodeURIComponent(savedChallenge.id)}/edit`)
2083+
const postSaveNavigationPath = resolvePostSaveNavigationPath({
2084+
isEditMode,
2085+
isSaveAsDraft,
2086+
redirectToViewOnSuccess: options.redirectToViewOnSuccess,
2087+
savedChallengeId: savedChallenge.id,
2088+
viewModePath,
2089+
})
2090+
2091+
if (postSaveNavigationPath) {
2092+
navigate(postSaveNavigationPath)
20412093
}
20422094
} catch (error) {
20432095
if (isHandledLaunchBlockError(error)) {
@@ -2075,6 +2127,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
20752127
setError,
20762128
syncDraftSingleAssignments,
20772129
usesManualReviewers,
2130+
viewModePath,
20782131
],
20792132
)
20802133

@@ -2182,7 +2235,9 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
21822235
}
21832236

21842237
clearErrors('reviewers')
2185-
await saveChallenge(formData)
2238+
await saveChallenge(formData, {
2239+
redirectToViewOnSuccess: true,
2240+
})
21862241
},
21872242
[
21882243
clearErrors,

0 commit comments

Comments
 (0)