Fix config save state sync — local config now re-syncs after save
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m46s

The save bar persisted forever because Zod.parse() adds defaults for
new fields, making the server config differ from local state. After
save, the sync effect now picks up the server value. Uses savingRef
to prevent overwriting local edits during the save roundtrip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-17 18:53:51 +01:00
parent cf1508f856
commit 4fa3ca0bb6

View File

@@ -237,16 +237,24 @@ export default function RoundDetailPage() {
{ enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 }, { enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 },
) )
// Initialize config from server once on load (or when round changes externally) // Initialize config from server on load; re-sync after saves
const serverConfig = useMemo(() => (round?.configJson as Record<string, unknown>) ?? {}, [round?.configJson]) const serverConfig = useMemo(() => (round?.configJson as Record<string, unknown>) ?? {}, [round?.configJson])
const configInitialized = useRef(false) const configInitialized = useRef(false)
if (round && !configInitialized.current) { const savingRef = useRef(false)
const roundConfig = (round.configJson as Record<string, unknown>) ?? {}
if (Object.keys(config).length === 0 || JSON.stringify(roundConfig) !== JSON.stringify(config)) { // Sync local config with server: on initial load AND whenever serverConfig
setConfig(roundConfig) // changes after a save completes (so Zod-applied defaults get picked up)
useEffect(() => {
if (!round) return
if (!configInitialized.current) {
configInitialized.current = true
setConfig(serverConfig)
} else if (!savingRef.current) {
// Server changed (e.g. after save invalidation) — re-sync
setConfig(serverConfig)
} }
configInitialized.current = true }, [serverConfig, round])
}
const hasUnsavedConfig = useMemo( const hasUnsavedConfig = useMemo(
() => configInitialized.current && JSON.stringify(config) !== JSON.stringify(serverConfig), () => configInitialized.current && JSON.stringify(config) !== JSON.stringify(serverConfig),
[config, serverConfig], [config, serverConfig],
@@ -255,11 +263,13 @@ export default function RoundDetailPage() {
// ── Mutations ────────────────────────────────────────────────────────── // ── Mutations ──────────────────────────────────────────────────────────
const updateMutation = trpc.round.update.useMutation({ const updateMutation = trpc.round.update.useMutation({
onSuccess: () => { onSuccess: () => {
savingRef.current = false
utils.round.getById.invalidate({ id: roundId }) utils.round.getById.invalidate({ id: roundId })
setAutosaveStatus('saved') setAutosaveStatus('saved')
setTimeout(() => setAutosaveStatus('idle'), 2000) setTimeout(() => setAutosaveStatus('idle'), 2000)
}, },
onError: (err) => { onError: (err) => {
savingRef.current = false
setAutosaveStatus('error') setAutosaveStatus('error')
toast.error(err.message) toast.error(err.message)
}, },
@@ -393,23 +403,27 @@ export default function RoundDetailPage() {
}, []) }, [])
const saveConfig = useCallback(() => { const saveConfig = useCallback(() => {
savingRef.current = true
setAutosaveStatus('saving') setAutosaveStatus('saving')
updateMutation.mutate({ id: roundId, configJson: config }) updateMutation.mutate({ id: roundId, configJson: config })
}, [config, roundId, updateMutation]) }, [config, roundId, updateMutation])
// ── Auto-save: debounce config changes and save automatically ──────── // ── Auto-save: debounce config changes and save automatically ────────
const configJson = JSON.stringify(config)
const serverJson = JSON.stringify(serverConfig)
useEffect(() => { useEffect(() => {
if (!configInitialized.current) return if (!configInitialized.current) return
if (JSON.stringify(config) === JSON.stringify(serverConfig)) return if (configJson === serverJson) return
const timer = setTimeout(() => { const timer = setTimeout(() => {
savingRef.current = true
setAutosaveStatus('saving') setAutosaveStatus('saving')
updateMutation.mutate({ id: roundId, configJson: config }) updateMutation.mutate({ id: roundId, configJson: config })
}, 800) }, 800)
return () => clearTimeout(timer) return () => clearTimeout(timer)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]) }, [configJson])
// ── Computed values ──────────────────────────────────────────────────── // ── Computed values ────────────────────────────────────────────────────
const projectCount = round?._count?.projectRoundStates ?? 0 const projectCount = round?._count?.projectRoundStates ?? 0