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], ) 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..1b903e818 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' @@ -1512,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), [ @@ -2927,8 +2929,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 +3410,18 @@ export const ChallengeEditorForm: FC = ( loginUserId, rawPaymentCreator, ]) + const reviewSection = usesManualReviewers ? (

Review

- +
) 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..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 @@ -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 | undefined) => 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 | undefined>(undefined) 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,72 @@ 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 = undefined + } + } + }, [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?.(undefined) + } + }, [flushPendingSave, onConfigSaveControllerReady]) + useEffect(() => { if (!normalizedChallengeId || !configurationMode || !normalizedConfiguration || readOnly) { return undefined @@ -1016,28 +1098,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..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 @@ -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 | undefined) => 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}