From fce7214dbb61d53513859b5dde6f2f062df15007 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 21 Apr 2026 10:35:18 +0300 Subject: [PATCH 1/2] PM-4879 - prevent saving with wrong template --- .../components/ChallengeEditorForm.spec.tsx | 102 ++++++++++++++++++ .../components/ChallengeEditorForm.tsx | 35 +++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx index 1a45ccaf7..0181e1f62 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx @@ -31,6 +31,7 @@ import { createChallenge, deleteResource, fetchAiReviewConfigByChallenge, + fetchAiReviewTemplates, fetchChallenge, fetchProfile, fetchProjectBillingAccount, @@ -69,6 +70,7 @@ jest.mock('../../../../lib/services', () => ({ createResource: jest.fn(), deleteResource: jest.fn(), fetchAiReviewConfigByChallenge: jest.fn(), + fetchAiReviewTemplates: jest.fn(), fetchChallenge: jest.fn(), fetchProfile: jest.fn(), fetchProjectBillingAccount: jest.fn(), @@ -573,6 +575,7 @@ const mockedCreateResource = createResource as jest.Mock const mockedCreateChallenge = createChallenge as jest.Mock const mockedDeleteResource = deleteResource as jest.Mock const mockedFetchAiReviewConfigByChallenge = fetchAiReviewConfigByChallenge as jest.Mock +const mockedFetchAiReviewTemplates = fetchAiReviewTemplates as jest.Mock const mockedFetchChallenge = fetchChallenge as jest.Mock const mockedFetchWorkflows = fetchWorkflows as jest.Mock const mockedFetchProfile = fetchProfile as jest.Mock @@ -684,6 +687,7 @@ describe('ChallengeEditorForm', () => { timelineTemplates: [], }) mockedFetchAiReviewConfigByChallenge.mockResolvedValue(undefined) + mockedFetchAiReviewTemplates.mockResolvedValue([]) mockedFetchWorkflows.mockResolvedValue([]) mockedFetchProjectBillingAccountService.mockResolvedValue({ billingAccount: undefined, @@ -2491,6 +2495,63 @@ describe('ChallengeEditorForm', () => { .toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.')) }) + it('blocks launching when the saved AI template has been disabled', async () => { + let launchAction: (() => Promise) | undefined + let launchError: Error | undefined + + mockedFetchAiReviewConfigByChallenge.mockResolvedValue({ + challengeId: '12345', + id: 'config-1', + minPassingThreshold: 75, + mode: 'AI_GATING', + templateId: 'template-disabled', + workflows: [], + }) + mockedFetchAiReviewTemplates.mockResolvedValue([{ + autoFinalize: false, + challengeTrack: 'DESIGN', + challengeType: 'First2Finish', + description: 'Disabled template', + disabled: true, + id: 'template-disabled', + minPassingThreshold: 75, + mode: 'AI_GATING', + title: 'Disabled template', + workflows: [], + }]) + + render( + + { + launchAction = action + }} + /> + , + ) + + await waitFor(() => { + expect(launchAction) + .toEqual(expect.any(Function)) + }) + + await act(async () => { + try { + await (launchAction as () => Promise)() + } catch (error) { + launchError = error as Error + } + }) + + expect(launchError?.message) + .toContain('The saved AI review template was disabled.') + expect(mockedPatchChallenge) + .not.toHaveBeenCalled() + expect(mockedShowErrorToast) + .toHaveBeenCalledWith(expect.stringContaining('The saved AI review template was disabled.')) + }) + it('does not render the attachments section while editing a draft', () => { render( @@ -2612,6 +2673,47 @@ describe('ChallengeEditorForm', () => { .toHaveBeenCalledWith(expect.stringContaining('One or more saved AI workflows were disabled.')) }) + it('blocks saving when the saved AI template has been disabled', async () => { + const user = userEvent.setup() + + mockedFetchAiReviewConfigByChallenge.mockResolvedValue({ + challengeId: '12345', + id: 'config-1', + minPassingThreshold: 75, + mode: 'AI_GATING', + templateId: 'template-disabled', + workflows: [], + }) + mockedFetchAiReviewTemplates.mockResolvedValue([{ + autoFinalize: false, + challengeTrack: 'DESIGN', + challengeType: 'First2Finish', + description: 'Disabled template', + disabled: true, + id: 'template-disabled', + minPassingThreshold: 75, + mode: 'AI_GATING', + title: 'Disabled template', + workflows: [], + }]) + + render( + + + , + ) + + await user.type(screen.getByLabelText('Challenge Name'), ' updated') + await user.click(screen.getByRole('button', { name: 'Save Challenge' })) + + await waitFor(() => { + expect(mockedPatchChallenge) + .not.toHaveBeenCalled() + }) + expect(mockedShowErrorToast) + .toHaveBeenCalledWith(expect.stringContaining('The saved AI review template was disabled.')) + }) + it('refreshes phase data when the fetched challenge updates for the same id', async () => { const initialChallenge = { ...validDraftChallenge, diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx index c2e18e4ef..7757fb3c9 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -54,6 +54,7 @@ import { createResource, deleteResource, fetchAiReviewConfigByChallenge, + fetchAiReviewTemplates, fetchChallenge, fetchProfile, fetchProjectBillingAccount, @@ -259,6 +260,9 @@ const TASK_ASSIGNED_MEMBER_REQUIRED_FOR_LAUNCH_MESSAGE const DISABLED_AI_WORKFLOW_FOR_CHALLENGE_ACTION_MESSAGE = 'One or more saved AI workflows were disabled. ' + 'Update the AI workflow configuration before saving or launching this challenge.' +const DISABLED_AI_TEMPLATE_FOR_CHALLENGE_ACTION_MESSAGE + = 'The saved AI review template was disabled. ' + + 'Update the AI template selection before saving or launching this challenge.' const CHALLENGE_TYPE_CHALLENGE_ABBREVIATION = 'CH' const CHALLENGE_TYPE_CHALLENGE_NAME = 'CHALLENGE' const CHALLENGE_TYPE_FIRST_2_FINISH_ABBREVIATION = 'F2F' @@ -1122,8 +1126,10 @@ function getReviewerValidationError( } async function getDisabledAiWorkflowForActionError( - challengeId: string | undefined, formData: ChallengeEditorFormData, + challengeId: string | undefined, + challengeTrack?: string, + challengeType?: string, ): Promise { const selectedAiWorkflowIds = (Array.isArray(formData.reviewers) ? formData.reviewers @@ -1142,6 +1148,29 @@ async function getDisabledAiWorkflowForActionError( ...selectedAiWorkflowIds, ...persistedWorkflowIds, ])) + const selectedTemplateId = normalizeTextValue(persistedAiConfig?.templateId) + + if (selectedTemplateId) { + const templates = await fetchAiReviewTemplates({ + challengeTrack, + challengeType, + }) + let selectedTemplate = templates.find(template => ( + normalizeTextValue(template.id) === selectedTemplateId + )) + + if (!selectedTemplate && (challengeTrack || challengeType)) { + const allTemplates = await fetchAiReviewTemplates() + + selectedTemplate = allTemplates.find(template => ( + normalizeTextValue(template.id) === selectedTemplateId + )) + } + + if (selectedTemplate?.disabled === true) { + return DISABLED_AI_TEMPLATE_FOR_CHALLENGE_ACTION_MESSAGE + } + } if (!configuredAiWorkflowIds.length) { return undefined @@ -2649,8 +2678,10 @@ export const ChallengeEditorForm: FC = ( } const disabledAiWorkflowError = await getDisabledAiWorkflowForActionError( - currentChallengeId, formData, + currentChallengeId, + selectedChallengeTrack?.track || selectedChallengeTrack?.name, + selectedChallengeType?.name, ) if (disabledAiWorkflowError) { From 570533328ad1e296e3cfdac27c0f6e0f914e4e6f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 21 Apr 2026 10:45:08 +0300 Subject: [PATCH 2/2] callback deps --- .../ChallengeEditorPage/components/ChallengeEditorForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx index 7757fb3c9..6c1f41a00 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -2814,6 +2814,8 @@ export const ChallengeEditorForm: FC = ( onChallengeStatusChange, reset, resolveProjectBillingAccount, + selectedChallengeTrack, + selectedChallengeType, setError, syncDraftSingleAssignments, usesManualReviewers,