From 43fa3828c857538843187b6195e4c98801ded0c5 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 15 Jun 2026 16:34:53 +0300 Subject: [PATCH 1/3] PM-5360 - fix reviewer getting saved --- .../form/FormUserAutocomplete/FormUserAutocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/work/src/lib/components/form/FormUserAutocomplete/FormUserAutocomplete.tsx b/src/apps/work/src/lib/components/form/FormUserAutocomplete/FormUserAutocomplete.tsx index d365dd7a8..f00dae36b 100644 --- a/src/apps/work/src/lib/components/form/FormUserAutocomplete/FormUserAutocomplete.tsx +++ b/src/apps/work/src/lib/components/form/FormUserAutocomplete/FormUserAutocomplete.tsx @@ -237,8 +237,8 @@ export const FormUserAutocomplete: FC = (props: FormU : '' setSelectedOption(nextSelectedOption) - field.onChange(nextValue) onValueChange?.(nextValue) + field.onChange(nextValue) }, [field, onValueChange], ) From c6d630cdd97cab6aebdc0f04d11c4e4dd81e9a17 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 15 Jun 2026 17:13:18 +0300 Subject: [PATCH 2/3] PM-5351 - ai screening phase creation --- .../components/ChallengeEditorForm.tsx | 18 ++- .../components/ReviewersField/AiReviewTab.tsx | 103 ++++++++++++++---- .../ReviewersField/ReviewersField.tsx | 4 +- 3 files changed, 100 insertions(+), 25 deletions(-) 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 23d61b77b..92c3246d8 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -167,6 +167,7 @@ import { import { ReviewersField, } from './ReviewersField' +import type { AiReviewConfigSaveController } from './ReviewersField/AiReviewTab' import { ReviewTypeField, } from './ReviewTypeField' @@ -2927,8 +2928,14 @@ export const ChallengeEditorForm: FC = ( throw createHandledLaunchBlockError(projectBillingAccountErrorMessage) } + let formDataToSave = formData + if (aiReviewConfigSaveControllerRef.current) { + await aiReviewConfigSaveControllerRef.current.flushPendingSave() + formDataToSave = getValues() + } + const formDataWithProjectBilling = applyProjectBillingToChallengeFormData( - formData, + formDataToSave, resolvedProjectBillingAccount, ) const payload = transformFormDataToChallenge({ @@ -3402,12 +3409,19 @@ export const ChallengeEditorForm: FC = ( loginUserId, rawPaymentCreator, ]) + const aiReviewConfigSaveControllerRef = useRef(null) + const reviewSection = usesManualReviewers ? (

Review

- + { + aiReviewConfigSaveControllerRef.current = controller + }} + />
) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx index 30cc9b9e5..e3a730cb7 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx @@ -47,6 +47,10 @@ import { } from './reviewers-field.utils' import styles from './AiReviewTab.module.scss' +export interface AiReviewConfigSaveController { + flushPendingSave: () => Promise +} + interface AiReviewTabProps { challengeId?: string hasSubmissions?: boolean @@ -55,6 +59,7 @@ interface AiReviewTabProps { trackId?: string typeId?: string onConfigPersisted?: (config: AiReviewConfig) => void + onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | null) => void } type ConfigurationMode = 'manual' | 'template' @@ -461,6 +466,7 @@ export const AiReviewTab: FC = ( const onConfigRemoved = props.onConfigRemoved const readOnly = props.hasSubmissions === true const onConfigPersisted = props.onConfigPersisted + const onConfigSaveControllerReady = props.onConfigSaveControllerReady const reviewers = props.reviewers const trackId = props.trackId const typeId = props.typeId @@ -470,6 +476,7 @@ export const AiReviewTab: FC = ( const initialConfigLookupChallengeIdRef = useRef() const onConfigPersistedRef = useRef(onConfigPersisted) const saveTimerRef = useRef | undefined>() + const configSavePromiseRef = useRef | null>(null) const [availableWorkflows, setAvailableWorkflows] = useState([]) const [configuration, setConfiguration] = useState(DEFAULT_CONFIGURATION) @@ -615,6 +622,15 @@ export const AiReviewTab: FC = ( templatesLoading, ], ) + + const hasPendingConfigurationChanges = useMemo( + (): boolean => ( + !!normalizedConfiguration + && aiReviewConfigHasChanges(lastSavedConfigurationRef.current, normalizedConfiguration) + && validationErrors.length === 0 + ), + [normalizedConfiguration, validationErrors], + ) const hasPersistedConfigForCurrentChallenge = useMemo( () => ( !!normalizeReviewerText(configId) @@ -999,6 +1015,71 @@ export const AiReviewTab: FC = ( } }, [configurationMode, selectedTrackName, selectedTypeName]) + const persistConfiguration = useCallback(async (): Promise => { + if (!normalizedConfiguration || readOnly || validationErrors.length > 0) { + return + } + + setIsSaving(true) + + const savePromise = (async (): Promise => { + try { + const savedConfiguration = configId + ? await updateAiReviewConfig(configId, normalizedConfiguration) + : await createAiReviewConfig(normalizedConfiguration) + const nextConfiguration = mapConfigToDraft(savedConfiguration) + + setConfigId(savedConfiguration.id) + setConfiguration(nextConfiguration) + lastSavedConfigurationRef.current = savedConfiguration + onConfigPersistedRef.current?.(savedConfiguration) + } catch (error) { + showErrorToast(error instanceof Error + ? error.message + : 'Failed to autosave AI review configuration') + } finally { + setIsSaving(false) + } + })() + + configSavePromiseRef.current = savePromise + try { + await savePromise + } finally { + if (configSavePromiseRef.current === savePromise) { + configSavePromiseRef.current = null + } + } + }, [configId, normalizedConfiguration, readOnly, validationErrors]) + + const flushPendingSave = useCallback(async (): Promise => { + if (configSavePromiseRef.current) { + await configSavePromiseRef.current.catch(() => undefined) + return + } + + if (!hasPendingConfigurationChanges) { + return + } + + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current) + saveTimerRef.current = undefined + } + + await persistConfiguration().catch(() => undefined) + }, [hasPendingConfigurationChanges, persistConfiguration]) + + useEffect(() => { + onConfigSaveControllerReady?.({ + flushPendingSave, + }) + + return () => { + onConfigSaveControllerReady?.(null) + } + }, [flushPendingSave, onConfigSaveControllerReady]) + useEffect(() => { if (!normalizedChallengeId || !configurationMode || !normalizedConfiguration || readOnly) { return undefined @@ -1016,28 +1097,6 @@ export const AiReviewTab: FC = ( } saveTimerRef.current = setTimeout(() => { - setIsSaving(true) - - const persistConfiguration = async (): Promise => { - try { - const savedConfiguration = configId - ? await updateAiReviewConfig(configId, normalizedConfiguration) - : await createAiReviewConfig(normalizedConfiguration) - const nextConfiguration = mapConfigToDraft(savedConfiguration) - - setConfigId(savedConfiguration.id) - setConfiguration(nextConfiguration) - lastSavedConfigurationRef.current = savedConfiguration - onConfigPersistedRef.current?.(savedConfiguration) - } catch (error) { - showErrorToast(error instanceof Error - ? error.message - : 'Failed to autosave AI review configuration') - } finally { - setIsSaving(false) - } - } - persistConfiguration() .catch(() => undefined) }, 1500) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx index b340fc2a1..05108713b 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx @@ -32,7 +32,7 @@ import { isAiReviewer, syncAiConfigReviewers, } from './reviewers-field.utils' -import AiReviewTab from './AiReviewTab' +import AiReviewTab, { AiReviewConfigSaveController } from './AiReviewTab' import HumanReviewTab from './HumanReviewTab' import ReviewConfigurationSummary from './ReviewConfigurationSummary' import styles from './ReviewersField.module.scss' @@ -41,6 +41,7 @@ type ReviewTab = 'ai' | 'human' interface ReviewersFieldProps { isReadOnly?: boolean + onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | null) => void } function hasReviewerChanges( @@ -387,6 +388,7 @@ export const ReviewersField: FC = (props: ReviewersFieldPro hasSubmissions={hasSubmissions} onConfigPersisted={handleAiConfigPersisted} onConfigRemoved={handleAiConfigRemoved} + onConfigSaveControllerReady={props.onConfigSaveControllerReady} reviewers={reviewerRows} trackId={trackId} typeId={typeId} From 037690701173f5724616c1ac70a87d366b82f025 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 15 Jun 2026 17:32:59 +0300 Subject: [PATCH 3/3] lint --- .../components/ChallengeEditorForm.tsx | 4 ++-- .../components/ReviewersField/AiReviewTab.tsx | 11 ++++++----- .../components/ReviewersField/ReviewersField.tsx | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) 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 92c3246d8..1b903e818 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -1513,6 +1513,7 @@ export const ChallengeEditorForm: FC = ( const challengeRef = useRef(props.challenge) const pendingChallengeRefreshRef = useRef() const defaultedDiscussionForumTypeIdRef = useRef() + const aiReviewConfigSaveControllerRef = useRef(undefined) const fallbackProjectId = useMemo( () => normalizeProjectId(props.projectId) || normalizeProjectId(props.challenge?.projectId), [ @@ -3409,7 +3410,6 @@ export const ChallengeEditorForm: FC = ( loginUserId, rawPaymentCreator, ]) - const aiReviewConfigSaveControllerRef = useRef(null) const reviewSection = usesManualReviewers ? ( @@ -3418,7 +3418,7 @@ export const ChallengeEditorForm: FC = (
{ + onConfigSaveControllerReady={function (controller: AiReviewConfigSaveController | undefined) { aiReviewConfigSaveControllerRef.current = controller }} /> diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx index e3a730cb7..f6e3329f5 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/AiReviewTab.tsx @@ -59,7 +59,7 @@ interface AiReviewTabProps { trackId?: string typeId?: string onConfigPersisted?: (config: AiReviewConfig) => void - onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | null) => void + onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | undefined) => void } type ConfigurationMode = 'manual' | 'template' @@ -476,7 +476,7 @@ export const AiReviewTab: FC = ( const initialConfigLookupChallengeIdRef = useRef() const onConfigPersistedRef = useRef(onConfigPersisted) const saveTimerRef = useRef | undefined>() - const configSavePromiseRef = useRef | null>(null) + const configSavePromiseRef = useRef | undefined>(undefined) const [availableWorkflows, setAvailableWorkflows] = useState([]) const [configuration, setConfiguration] = useState(DEFAULT_CONFIGURATION) @@ -1047,7 +1047,7 @@ export const AiReviewTab: FC = ( await savePromise } finally { if (configSavePromiseRef.current === savePromise) { - configSavePromiseRef.current = null + configSavePromiseRef.current = undefined } } }, [configId, normalizedConfiguration, readOnly, validationErrors]) @@ -1067,7 +1067,8 @@ export const AiReviewTab: FC = ( saveTimerRef.current = undefined } - await persistConfiguration().catch(() => undefined) + await persistConfiguration() + .catch(() => undefined) }, [hasPendingConfigurationChanges, persistConfiguration]) useEffect(() => { @@ -1076,7 +1077,7 @@ export const AiReviewTab: FC = ( }) return () => { - onConfigSaveControllerReady?.(null) + onConfigSaveControllerReady?.(undefined) } }, [flushPendingSave, onConfigSaveControllerReady]) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx index 05108713b..a6191d787 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ReviewersField/ReviewersField.tsx @@ -41,7 +41,7 @@ type ReviewTab = 'ai' | 'human' interface ReviewersFieldProps { isReadOnly?: boolean - onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | null) => void + onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | undefined) => void } function hasReviewerChanges(