style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -20,21 +20,21 @@
|
||||
* T-03-18 — failed/dead toast persists; no silent loss.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2, Check, AlertCircle, X } from 'lucide-react'
|
||||
import { useCalendarStore } from '../store/calendarStore.js'
|
||||
import { fetchSyncStatus, type SyncStatus } from '../api/client.js'
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Check, AlertCircle, X } from 'lucide-react';
|
||||
import { useCalendarStore } from '../store/calendarStore.js';
|
||||
import { fetchSyncStatus, type SyncStatus } from '../api/client.js';
|
||||
|
||||
// ── Component ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function SyncStateToast() {
|
||||
const lastSyncedUid = useCalendarStore((s) => s.lastSyncedUid)
|
||||
const setLastSyncedUid = useCalendarStore((s) => s.setLastSyncedUid)
|
||||
const queryClient = useQueryClient()
|
||||
const lastSyncedUid = useCalendarStore((s) => s.lastSyncedUid);
|
||||
const setLastSyncedUid = useCalendarStore((s) => s.setLastSyncedUid);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Track whether we've already invalidated for this uid to avoid duplicate calls
|
||||
const invalidatedRef = useRef<string | null>(null)
|
||||
const invalidatedRef = useRef<string | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery<SyncStatus>({
|
||||
queryKey: ['syncStatus', lastSyncedUid],
|
||||
@@ -42,113 +42,107 @@ export function SyncStateToast() {
|
||||
enabled: lastSyncedUid !== null,
|
||||
// refetchInterval: active (3000ms) only while pending; disabled once terminal
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status
|
||||
return status === 'pending' || status === undefined ? 3000 : false
|
||||
const status = query.state.data?.status;
|
||||
return status === 'pending' || status === undefined ? 3000 : false;
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
});
|
||||
|
||||
const status = data?.status
|
||||
const status = data?.status;
|
||||
// WR-06: an edit-as-move whose create hits 412 dead-ends with no retry path (the
|
||||
// original event is preserved, but the new uid's only outbox row is failed). The
|
||||
// worker tags that case with a 'move-failed:' lastError so we can show distinct copy
|
||||
// guiding the user to re-open and re-save, rather than the etag-conflict copy.
|
||||
const isMoveFailed = status === 'failed' && !!data?.error?.startsWith('move-failed')
|
||||
const isConflict = status === 'failed' && !isMoveFailed && data?.error?.includes('412')
|
||||
const isPersistent = status === 'failed' || status === 'dead'
|
||||
const isMoveFailed = status === 'failed' && !!data?.error?.startsWith('move-failed');
|
||||
const isConflict = status === 'failed' && !isMoveFailed && data?.error?.includes('412');
|
||||
const isPersistent = status === 'failed' || status === 'dead';
|
||||
|
||||
// Invalidate events on done OR on conflict (D-06/D-08)
|
||||
useEffect(() => {
|
||||
if (!lastSyncedUid) return
|
||||
if (invalidatedRef.current === lastSyncedUid) return
|
||||
if (!lastSyncedUid) return;
|
||||
if (invalidatedRef.current === lastSyncedUid) return;
|
||||
|
||||
if (status === 'done' || isConflict) {
|
||||
invalidatedRef.current = lastSyncedUid
|
||||
void queryClient.invalidateQueries({ queryKey: ['events'] })
|
||||
invalidatedRef.current = lastSyncedUid;
|
||||
void queryClient.invalidateQueries({ queryKey: ['events'] });
|
||||
}
|
||||
}, [status, isConflict, lastSyncedUid, queryClient])
|
||||
}, [status, isConflict, lastSyncedUid, queryClient]);
|
||||
|
||||
// Auto-dismiss after 2s on done
|
||||
useEffect(() => {
|
||||
if (status !== 'done') return
|
||||
if (status !== 'done') return;
|
||||
const timer = setTimeout(() => {
|
||||
setLastSyncedUid(null)
|
||||
}, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [status, setLastSyncedUid])
|
||||
setLastSyncedUid(null);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [status, setLastSyncedUid]);
|
||||
|
||||
// Reset invalidation guard when uid changes (new write)
|
||||
useEffect(() => {
|
||||
if (lastSyncedUid === null) {
|
||||
invalidatedRef.current = null
|
||||
invalidatedRef.current = null;
|
||||
}
|
||||
}, [lastSyncedUid])
|
||||
}, [lastSyncedUid]);
|
||||
|
||||
// Nothing to show
|
||||
if (!lastSyncedUid || (!isLoading && !data)) return null
|
||||
if (!lastSyncedUid || (!isLoading && !data)) return null;
|
||||
|
||||
// ── State-specific content ─────────────────────────────────────────────────
|
||||
|
||||
const dismiss = () => setLastSyncedUid(null)
|
||||
const dismiss = () => setLastSyncedUid(null);
|
||||
|
||||
// Determine ARIA role: status (polite) for pending/done; alert (assertive) for failed/dead
|
||||
const ariaRole: 'status' | 'alert' =
|
||||
status === 'failed' || status === 'dead' ? 'alert' : 'status'
|
||||
status === 'failed' || status === 'dead' ? 'alert' : 'status';
|
||||
|
||||
// Toast copy (exact strings from UI-SPEC §Copywriting)
|
||||
let copy: string
|
||||
let icon: React.ReactNode
|
||||
let copy: string;
|
||||
let icon: React.ReactNode;
|
||||
|
||||
if (status === 'done') {
|
||||
copy = 'Saved'
|
||||
icon = (
|
||||
<Check
|
||||
size={14}
|
||||
style={{ color: '#50C878', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
copy = 'Saved';
|
||||
icon = <Check size={14} style={{ color: '#50C878', flexShrink: 0 }} aria-hidden="true" />;
|
||||
} else if (isConflict) {
|
||||
copy = 'This event changed elsewhere — review the latest version'
|
||||
copy = 'This event changed elsewhere — review the latest version';
|
||||
icon = (
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{ color: 'var(--color-destructive)', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
} else if (isMoveFailed) {
|
||||
// WR-06: the move could not be applied; the original event is unchanged.
|
||||
copy = "Couldn't move the event. Open it and save again."
|
||||
copy = "Couldn't move the event. Open it and save again.";
|
||||
icon = (
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{ color: 'var(--color-destructive)', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
} else if (status === 'failed') {
|
||||
copy = "Didn't save. Try again."
|
||||
copy = "Didn't save. Try again.";
|
||||
icon = (
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{ color: 'var(--color-destructive)', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
} else if (status === 'dead') {
|
||||
copy = 'Not saved. Check your connection.'
|
||||
copy = 'Not saved. Check your connection.';
|
||||
icon = (
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{ color: 'var(--color-destructive)', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// pending (or loading)
|
||||
copy = 'Syncing…'
|
||||
copy = 'Syncing…';
|
||||
icon = (
|
||||
<Loader2
|
||||
size={14}
|
||||
@@ -159,7 +153,7 @@ export function SyncStateToast() {
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -221,5 +215,5 @@ export function SyncStateToast() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user