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