Phase 1b: React Query + Vitest on client

- @tanstack/react-query v5 with QueryClientProvider at app root
- client/src/api/queries.ts: query-key factory, hooks for tickets, ticket, audit,
  comments, users, CTI tree + cascade, plus full mutation set
  (create/update/delete ticket, add/delete comment, CTI CRUD, user CRUD)
- All page-level useEffect + useState fetching replaced:
  Dashboard, MyTickets, TicketDetail, NewTicket, admin/CTI, admin/Users
- Dashboard preserves 300ms debounced search via separate debouncedSearch state
- CTISelect cascades via useCategories / useTypes(categoryId) / useItems(typeId);
  dependent hooks disabled until parent selected
- vitest + @testing-library/react + jsdom; 6 client tests cover SeverityBadge + StatusBadge

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:35:09 -04:00
parent aff52e5672
commit 4eae11b5b0
16 changed files with 1722 additions and 3401 deletions
+22 -25
View File
@@ -1,10 +1,15 @@
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { Plus, Pencil, Trash2, RefreshCw, Copy, Check } from 'lucide-react';
import api from '../../api/client';
import Layout from '../../components/Layout';
import Modal from '../../components/Modal';
import { User, Role } from '../../types';
import { useAuth } from '../../contexts/AuthContext';
import {
useUsers,
useCreateUser,
useUpdateUser,
useDeleteUser,
} from '../../api/queries';
interface UserForm {
username: string;
@@ -45,22 +50,19 @@ const ROLE_DESCRIPTIONS: Record<Role, string> = {
export default function AdminUsers() {
const { user: authUser } = useAuth();
const [users, setUsers] = useState<User[]>([]);
const { data: users = [] } = useUsers();
const createUser = useCreateUser();
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
const [modal, setModal] = useState<'add' | 'edit' | null>(null);
const [selected, setSelected] = useState<User | null>(null);
const [form, setForm] = useState<UserForm>(BLANK_FORM);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const [newApiKey, setNewApiKey] = useState<string | null>(null);
const fetchUsers = () => {
api.get<User[]>('/users').then((r) => setUsers(r.data));
};
useEffect(() => {
fetchUsers();
}, []);
const submitting = createUser.isPending || updateUser.isPending;
const openAdd = () => {
setForm(BLANK_FORM);
@@ -87,12 +89,10 @@ export default function AdminUsers() {
setModal(null);
setSelected(null);
setNewApiKey(null);
fetchUsers();
};
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setError('');
try {
const payload: Record<string, string> = {
@@ -102,21 +102,18 @@ export default function AdminUsers() {
role: form.role,
};
if (form.password) payload.password = form.password;
const res = await api.post<User>('/users', payload);
if (res.data.apiKey) setNewApiKey(res.data.apiKey);
const created = await createUser.mutateAsync(payload);
if (created.apiKey) setNewApiKey(created.apiKey);
else closeModal();
} catch (err: unknown) {
const e = err as { response?: { data?: { error?: string } } };
setError(e.response?.data?.error ?? 'Failed to create user');
} finally {
setSubmitting(false);
}
};
const handleEdit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selected) return;
setSubmitting(true);
setError('');
try {
const payload: Record<string, string> = {
@@ -125,20 +122,17 @@ export default function AdminUsers() {
role: form.role,
};
if (form.password) payload.password = form.password;
await api.patch(`/users/${selected.id}`, payload);
await updateUser.mutateAsync({ id: selected.id, data: payload });
closeModal();
} catch (err: unknown) {
const e = err as { response?: { data?: { error?: string } } };
setError(e.response?.data?.error ?? 'Failed to update user');
} finally {
setSubmitting(false);
}
};
const handleDelete = async (u: User) => {
if (!confirm(`Delete user "${u.displayName}"?`)) return;
await api.delete(`/users/${u.id}`);
fetchUsers();
await deleteUser.mutateAsync(u.id);
};
const handleRegenerateKey = async (u: User) => {
@@ -148,8 +142,11 @@ export default function AdminUsers() {
)
)
return;
const res = await api.patch<User>(`/users/${u.id}`, { regenerateApiKey: true });
setNewApiKey(res.data.apiKey ?? null);
const updated = await updateUser.mutateAsync({
id: u.id,
data: { regenerateApiKey: true },
});
setNewApiKey(updated.apiKey ?? null);
setSelected(u);
setModal('edit');
};