From 4fa3ca0bb682c5f02b7b235b89ab61ef4e78c569 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Feb 2026 18:53:51 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20config=20save=20state=20sync=20=E2=80=94?= =?UTF-8?q?=20local=20config=20now=20re-syncs=20after=20save?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../(admin)/admin/rounds/[roundId]/page.tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 48c79f7..c7e3618 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -237,16 +237,24 @@ export default function RoundDetailPage() { { 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) ?? {}, [round?.configJson]) const configInitialized = useRef(false) - if (round && !configInitialized.current) { - const roundConfig = (round.configJson as Record) ?? {} - if (Object.keys(config).length === 0 || JSON.stringify(roundConfig) !== JSON.stringify(config)) { - setConfig(roundConfig) + const savingRef = useRef(false) + + // Sync local config with server: on initial load AND whenever serverConfig + // 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( () => configInitialized.current && JSON.stringify(config) !== JSON.stringify(serverConfig), [config, serverConfig], @@ -255,11 +263,13 @@ export default function RoundDetailPage() { // ── Mutations ────────────────────────────────────────────────────────── const updateMutation = trpc.round.update.useMutation({ onSuccess: () => { + savingRef.current = false utils.round.getById.invalidate({ id: roundId }) setAutosaveStatus('saved') setTimeout(() => setAutosaveStatus('idle'), 2000) }, onError: (err) => { + savingRef.current = false setAutosaveStatus('error') toast.error(err.message) }, @@ -393,23 +403,27 @@ export default function RoundDetailPage() { }, []) const saveConfig = useCallback(() => { + savingRef.current = true setAutosaveStatus('saving') updateMutation.mutate({ id: roundId, configJson: config }) }, [config, roundId, updateMutation]) // ── Auto-save: debounce config changes and save automatically ──────── + const configJson = JSON.stringify(config) + const serverJson = JSON.stringify(serverConfig) useEffect(() => { if (!configInitialized.current) return - if (JSON.stringify(config) === JSON.stringify(serverConfig)) return + if (configJson === serverJson) return const timer = setTimeout(() => { + savingRef.current = true setAutosaveStatus('saving') updateMutation.mutate({ id: roundId, configJson: config }) }, 800) return () => clearTimeout(timer) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]) + }, [configJson]) // ── Computed values ──────────────────────────────────────────────────── const projectCount = round?._count?.projectRoundStates ?? 0