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:
2026-05-12 23:34:24 +02:00
parent d1c9469fa7
commit 6ca94ee3f1
7 changed files with 126 additions and 169 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { type ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2, Plus, ShieldCheck, ShieldOff, Power, PowerOff } from 'lucide-react';
@@ -27,27 +28,36 @@ interface UserRow {
assignedAt: string;
}
const USERS_QUERY_KEY = ['admin', 'users'] as const;
export function UserList() {
const [users, setUsers] = useState<UserRow[]>([]);
const [loading, setLoading] = useState(true);
const queryClient = useQueryClient();
const [formOpen, setFormOpen] = useState(false);
const [editingUser, setEditingUser] = useState<UserRow | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [togglingId, setTogglingId] = useState<string | null>(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const res = await apiFetch<{ data: UserRow[] }>('/api/v1/admin/users');
setUsers(res.data);
} finally {
setLoading(false);
}
}, []);
const { data: users = [], isLoading: loading } = useQuery<UserRow[]>({
queryKey: USERS_QUERY_KEY,
queryFn: () => apiFetch<{ data: UserRow[] }>('/api/v1/admin/users').then((r) => r.data),
});
useEffect(() => {
void fetchUsers();
}, [fetchUsers]);
const fetchUsers = () => queryClient.invalidateQueries({ queryKey: USERS_QUERY_KEY });
const removeMutation = useMutation({
mutationFn: (userId: string) => apiFetch(`/api/v1/admin/users/${userId}`, { method: 'DELETE' }),
onSuccess: () => fetchUsers(),
});
const toggleMutation = useMutation({
mutationFn: (user: UserRow) =>
apiFetch(`/api/v1/admin/users/${user.userId}`, {
method: 'PATCH',
body: { isActive: !user.isActive },
}),
onSuccess: () => fetchUsers(),
});
const deletingId = removeMutation.isPending ? removeMutation.variables : null;
const togglingId = toggleMutation.isPending ? (toggleMutation.variables?.userId ?? null) : null;
function handleNewUser() {
setEditingUser(null);
@@ -59,27 +69,12 @@ export function UserList() {
setFormOpen(true);
}
async function handleRemoveUser(userId: string) {
setDeletingId(userId);
try {
await apiFetch(`/api/v1/admin/users/${userId}`, { method: 'DELETE' });
await fetchUsers();
} finally {
setDeletingId(null);
}
function handleRemoveUser(userId: string) {
removeMutation.mutate(userId);
}
async function handleToggleActive(user: UserRow) {
setTogglingId(user.userId);
try {
await apiFetch(`/api/v1/admin/users/${user.userId}`, {
method: 'PATCH',
body: { isActive: !user.isActive },
});
await fetchUsers();
} finally {
setTogglingId(null);
}
function handleToggleActive(user: UserRow) {
toggleMutation.mutate(user);
}
const columns: ColumnDef<UserRow, unknown>[] = [