Fix evaluation double-click submit: autosave was blocking the submit button
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m35s

Root cause: submit button was disabled when autosaveMutation.isPending was true.
If the 3-second autosave timer fired while the user clicked Submit, the click
was silently swallowed. User had to wait for autosave to finish, then click again.

Fixes:
- Remove autosaveMutation.isPending from submit button disabled state
- Cancel pending autosave timer when submit starts (prevents race condition)
- Add isSubmittingRef guard to prevent autosave from firing during submit
- Reset submitting flag on validation failure or submit error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 19:14:37 +01:00
parent 3bc6552f47
commit 61c4d0eb75

View File

@@ -40,6 +40,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
const isDirtyRef = useRef(false)
const evaluationIdRef = useRef<string | null>(null)
const isSubmittedRef = useRef(false)
const isSubmittingRef = useRef(false)
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null)
@@ -203,7 +204,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
// Perform autosave
const performAutosave = useCallback(async () => {
if (!isDirtyRef.current || isSubmittedRef.current) return
if (!isDirtyRef.current || isSubmittedRef.current || isSubmittingRef.current) return
if (existingEvaluation?.status === 'SUBMITTED') return
let evalId = evaluationIdRef.current
@@ -311,8 +312,16 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
}
const handleSubmit = async () => {
// Cancel any pending autosave to avoid race conditions
if (autosaveTimerRef.current) {
clearTimeout(autosaveTimerRef.current)
autosaveTimerRef.current = null
}
isSubmittingRef.current = true
if (!myAssignment) {
toast.error('Assignment not found')
isSubmittingRef.current = false
return
}
@@ -325,14 +334,17 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
const val = criteriaValues[c.id]
if (c.type === 'numeric' && (val === undefined || val === null)) {
toast.error(`Please score "${c.label}"`)
isSubmittingRef.current = false
return
}
if (c.type === 'boolean' && val === undefined) {
toast.error(`Please answer "${c.label}"`)
isSubmittingRef.current = false
return
}
if (c.type === 'text' && (!val || (typeof val === 'string' && !val.trim()))) {
toast.error(`Please fill in "${c.label}"`)
isSubmittingRef.current = false
return
}
}
@@ -342,6 +354,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
const score = parseInt(globalScore, 10)
if (isNaN(score) || score < 1 || score > 10) {
toast.error('Please enter a valid score between 1 and 10')
isSubmittingRef.current = false
return
}
}
@@ -349,6 +362,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
if (scoringMode === 'binary') {
if (!binaryDecision) {
toast.error('Please select accept or reject')
isSubmittingRef.current = false
return
}
}
@@ -356,6 +370,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
if (requireFeedback) {
if (!feedbackText.trim() || feedbackText.length < feedbackMinLength) {
toast.error(`Please provide feedback (minimum ${feedbackMinLength} characters)`)
isSubmittingRef.current = false
return
}
}
@@ -398,6 +413,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
})
} catch {
// Error toast already handled by onError callback
isSubmittingRef.current = false
}
}
@@ -862,7 +878,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</Button>
<Button
onClick={handleSubmit}
disabled={submitMutation.isPending || autosaveMutation.isPending || startMutation.isPending}
disabled={submitMutation.isPending || startMutation.isPending}
className="bg-brand-blue hover:bg-brand-blue-light"
>
<Send className="mr-2 h-4 w-4" />