From 8f777dd15ac5ef64ee15d03c5769cac9375a59d9 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Fri, 6 Mar 2026 17:47:26 +0800 Subject: [PATCH] Fix lock banner: use viewLockQuery.data directly instead of syncing through state Root cause: the previous approach synced poll data into lockInfo via a useEffect. When the user selected an event with cached lock data, both the poll-data effect and the event-change reset effect ran in the same render cycle. The event-change effect ran second (effects are ordered by definition) and cleared lockInfo to null. On the next render, viewLockQuery.data hadn't changed (TanStack Query structural sharing returns same reference), so the poll-data effect never re-fired. Result: lockInfo stayed null, banner stayed hidden until the next polling interval returned new data. Fix: derive activeLockInfo directly from viewLockQuery.data (structural sharing means it's always the latest authoritative value from TanStack Query) with lockInfo as a fallback for the 423-error path only. Also add refetchIntervalInBackground:true and refetchOnMount:'always' to ensure polling doesn't pause on tab switch and always fires a fresh fetch when the component mounts. Co-Authored-By: Claude Opus 4.6 --- .../components/calendar/EventDetailPanel.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/calendar/EventDetailPanel.tsx b/frontend/src/components/calendar/EventDetailPanel.tsx index b656fbe..8362a1f 100644 --- a/frontend/src/components/calendar/EventDetailPanel.tsx +++ b/frontend/src/components/calendar/EventDetailPanel.tsx @@ -264,6 +264,7 @@ export default function EventDetailPanel({ const [locationSearch, setLocationSearch] = useState(''); // Poll lock status in view mode for shared events (Stream A: real-time lock awareness) + // lockInfo is only set from the 423 error path; poll data (viewLockQuery.data) is used directly. const viewLockQuery = useQuery({ queryKey: ['event-lock', event?.id], queryFn: async () => { @@ -274,18 +275,22 @@ export default function EventDetailPanel({ }, enabled: !!isSharedEvent && !!event && typeof event.id === 'number' && !isEditing && !isCreating, refetchInterval: 5_000, + refetchIntervalInBackground: true, + refetchOnMount: 'always', }); - // Show/hide lock banner proactively in view mode (poll data always authoritative) + // Clear 423-error lockInfo when poll confirms lock is gone useEffect(() => { - if (!viewLockQuery.data) return; - if (viewLockQuery.data.locked) { - setLockInfo(viewLockQuery.data); - } else { + if (viewLockQuery.data && !viewLockQuery.data.locked) { setLockInfo(null); } }, [viewLockQuery.data]); + // Derived: authoritative lock state — poll data wins, 423 error lockInfo as fallback + const activeLockInfo: EventLockInfo | null = + (viewLockQuery.data?.locked ? viewLockQuery.data : null) ?? + (lockInfo?.locked ? lockInfo : null); + const isRecurring = !!(event?.is_recurring || event?.parent_event_id); // Permission helpers @@ -580,8 +585,8 @@ export default function EventDetailPanel({ size="icon" className="h-7 w-7" onClick={handleEditStart} - disabled={isAcquiringLock || !!(lockInfo && lockInfo.locked)} - title={lockInfo && lockInfo.locked ? `Locked by ${lockInfo.locked_by_name || 'another user'}` : 'Edit event'} + disabled={isAcquiringLock || !!activeLockInfo} + title={activeLockInfo ? `Locked by ${activeLockInfo.locked_by_name || 'another user'}` : 'Edit event'} > {isAcquiringLock ? : } @@ -629,12 +634,12 @@ export default function EventDetailPanel({ {/* Body */}
- {/* Lock banner */} - {lockInfo && lockInfo.locked && ( + {/* Lock banner — shown when activeLockInfo reports a lock (poll-authoritative) */} + {activeLockInfo && ( )}