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 <noreply@anthropic.com>
This commit is contained in:
parent
3dcf9d1671
commit
8f777dd15a
@ -264,6 +264,7 @@ export default function EventDetailPanel({
|
|||||||
const [locationSearch, setLocationSearch] = useState('');
|
const [locationSearch, setLocationSearch] = useState('');
|
||||||
|
|
||||||
// Poll lock status in view mode for shared events (Stream A: real-time lock awareness)
|
// 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({
|
const viewLockQuery = useQuery({
|
||||||
queryKey: ['event-lock', event?.id],
|
queryKey: ['event-lock', event?.id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@ -274,18 +275,22 @@ export default function EventDetailPanel({
|
|||||||
},
|
},
|
||||||
enabled: !!isSharedEvent && !!event && typeof event.id === 'number' && !isEditing && !isCreating,
|
enabled: !!isSharedEvent && !!event && typeof event.id === 'number' && !isEditing && !isCreating,
|
||||||
refetchInterval: 5_000,
|
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(() => {
|
useEffect(() => {
|
||||||
if (!viewLockQuery.data) return;
|
if (viewLockQuery.data && !viewLockQuery.data.locked) {
|
||||||
if (viewLockQuery.data.locked) {
|
|
||||||
setLockInfo(viewLockQuery.data);
|
|
||||||
} else {
|
|
||||||
setLockInfo(null);
|
setLockInfo(null);
|
||||||
}
|
}
|
||||||
}, [viewLockQuery.data]);
|
}, [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);
|
const isRecurring = !!(event?.is_recurring || event?.parent_event_id);
|
||||||
|
|
||||||
// Permission helpers
|
// Permission helpers
|
||||||
@ -580,8 +585,8 @@ export default function EventDetailPanel({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleEditStart}
|
onClick={handleEditStart}
|
||||||
disabled={isAcquiringLock || !!(lockInfo && lockInfo.locked)}
|
disabled={isAcquiringLock || !!activeLockInfo}
|
||||||
title={lockInfo && lockInfo.locked ? `Locked by ${lockInfo.locked_by_name || 'another user'}` : 'Edit event'}
|
title={activeLockInfo ? `Locked by ${activeLockInfo.locked_by_name || 'another user'}` : 'Edit event'}
|
||||||
>
|
>
|
||||||
{isAcquiringLock ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Pencil className="h-3.5 w-3.5" />}
|
{isAcquiringLock ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Pencil className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
@ -629,12 +634,12 @@ export default function EventDetailPanel({
|
|||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-3">
|
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-3">
|
||||||
{/* Lock banner */}
|
{/* Lock banner — shown when activeLockInfo reports a lock (poll-authoritative) */}
|
||||||
{lockInfo && lockInfo.locked && (
|
{activeLockInfo && (
|
||||||
<EventLockBanner
|
<EventLockBanner
|
||||||
lockedByName={lockInfo.locked_by_name || 'another user'}
|
lockedByName={activeLockInfo.locked_by_name || 'another user'}
|
||||||
expiresAt={lockInfo.expires_at}
|
expiresAt={activeLockInfo.expires_at}
|
||||||
isPermanent={lockInfo.is_permanent}
|
isPermanent={activeLockInfo.is_permanent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user