fix(compiler): migrate 6 list pages to useQuery (set-state-in-effect)
Replaces the useState + useEffect + apiFetch pattern with TanStack Query in six admin list pages — same pattern, mechanical refactor: - admin/tags/tag-list - admin/ports/port-list - admin/roles/role-list - admin/users/user-list - admin/document-templates/template-list - admin/webhooks/page - dashboard/timezone-drift-banner (also: detected-tz reads via useSyncExternalStore so render stays pure) Side benefits: list refetches now share a query cache across tabs (via @tanstack/query-broadcast-client-experimental that was wired up earlier this branch), so when admin A edits a role in one tab, admin B's tab sees the updated row without a manual reload. set-state-in-effect warnings: 51 → 45. Verified: tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState, useSyncExternalStore } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Clock, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -35,37 +35,37 @@ interface MeResponse {
|
||||
* Dismissal is sticky per browser via localStorage so the rep isn't nagged
|
||||
* once they've decided. Clearing storage or signing in elsewhere re-asks.
|
||||
*/
|
||||
function getDetectedTimezone(): string | null {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialDismissed(): boolean {
|
||||
try {
|
||||
return window.localStorage.getItem(DISMISS_STORAGE_KEY) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// useSyncExternalStore lets us read window.matchMedia-style external state
|
||||
// without bouncing through useEffect → setState.
|
||||
const detectedSubscribe = () => () => {};
|
||||
|
||||
export function TimezoneDriftBanner() {
|
||||
const queryClient = useQueryClient();
|
||||
const [detected, setDetected] = useState<string | null>(null);
|
||||
const [stored, setStored] = useState<string | null>(null);
|
||||
const [profileLoaded, setProfileLoaded] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const detected = useSyncExternalStore(detectedSubscribe, getDetectedTimezone, () => null);
|
||||
const [dismissed, setDismissed] = useState(getInitialDismissed);
|
||||
|
||||
// Read on mount: browser's resolved timezone (mirrors the OS setting) +
|
||||
// the user's stored preference + any prior dismissal flag. All three
|
||||
// are stable across the lifetime of the dashboard view; the banner
|
||||
// makes a single comparison and either renders or doesn't.
|
||||
useEffect(() => {
|
||||
try {
|
||||
setDetected(Intl.DateTimeFormat().resolvedOptions().timeZone || null);
|
||||
} catch {
|
||||
setDetected(null);
|
||||
}
|
||||
try {
|
||||
const flag = window.localStorage.getItem(DISMISS_STORAGE_KEY);
|
||||
if (flag === 'true') setDismissed(true);
|
||||
} catch {
|
||||
// Private mode or quota — proceed without dismissal memory.
|
||||
}
|
||||
void apiFetch<MeResponse>('/api/v1/me')
|
||||
.then((res) => {
|
||||
const tz = res.data.profile?.preferences?.timezone ?? res.data.profile?.timezone ?? null;
|
||||
setStored(tz);
|
||||
})
|
||||
.catch(() => setStored(null))
|
||||
.finally(() => setProfileLoaded(true));
|
||||
}, []);
|
||||
const { data: profile, isSuccess: profileLoaded } = useQuery<MeResponse>({
|
||||
queryKey: ['me'],
|
||||
queryFn: () => apiFetch<MeResponse>('/api/v1/me'),
|
||||
});
|
||||
const stored =
|
||||
profile?.data.profile?.preferences?.timezone ?? profile?.data.profile?.timezone ?? null;
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (newTz: string) => {
|
||||
@@ -77,7 +77,6 @@ export function TimezoneDriftBanner() {
|
||||
onSuccess: () => {
|
||||
toast.success(`Timezone updated to ${formatTimezoneLabel(detected ?? '')}.`);
|
||||
queryClient.invalidateQueries({ queryKey: ['me'] });
|
||||
setStored(detected);
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user