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, Lock } from 'lucide-react';
@@ -31,27 +32,25 @@ interface Role {
createdAt: string;
}
const ROLES_QUERY_KEY = ['admin', 'roles'] as const;
export function RoleList() {
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const queryClient = useQueryClient();
const [formOpen, setFormOpen] = useState(false);
const [editingRole, setEditingRole] = useState<Role | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [viewingPermissions, setViewingPermissions] = useState<Role | null>(null);
const fetchRoles = useCallback(async () => {
setLoading(true);
try {
const res = await apiFetch<{ data: Role[] }>('/api/v1/admin/roles');
setRoles(res.data);
} finally {
setLoading(false);
}
}, []);
const { data: roles = [], isLoading: loading } = useQuery<Role[]>({
queryKey: ROLES_QUERY_KEY,
queryFn: () => apiFetch<{ data: Role[] }>('/api/v1/admin/roles').then((r) => r.data),
});
useEffect(() => {
void fetchRoles();
}, [fetchRoles]);
const deleteMutation = useMutation({
mutationFn: (id: string) => apiFetch(`/api/v1/admin/roles/${id}`, { method: 'DELETE' }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ROLES_QUERY_KEY }),
});
const fetchRoles = () => queryClient.invalidateQueries({ queryKey: ROLES_QUERY_KEY });
function handleNewRole() {
setEditingRole(null);
@@ -63,16 +62,6 @@ export function RoleList() {
setFormOpen(true);
}
async function handleDeleteRole(id: string) {
setDeletingId(id);
try {
await apiFetch(`/api/v1/admin/roles/${id}`, { method: 'DELETE' });
await fetchRoles();
} finally {
setDeletingId(null);
}
}
function countPermissions(perms: Record<string, Record<string, boolean>>): string {
let granted = 0;
let total = 0;
@@ -155,8 +144,8 @@ export function RoleList() {
title="Delete Role"
description={`Delete "${row.original.name}"? Users assigned to this role must be reassigned first.`}
confirmLabel="Delete"
onConfirm={() => handleDeleteRole(row.original.id)}
loading={deletingId === row.original.id}
onConfirm={() => deleteMutation.mutate(row.original.id)}
loading={deleteMutation.isPending && deleteMutation.variables === row.original.id}
/>
)}
</div>