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:
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Search, ChevronRight, X } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import api from '../api/client';
|
||||
import Layout from '../components/Layout';
|
||||
import SeverityBadge from '../components/SeverityBadge';
|
||||
import StatusBadge from '../components/StatusBadge';
|
||||
import Avatar from '../components/Avatar';
|
||||
import { Ticket, TicketStatus, Category, CTIType, Item } from '../types';
|
||||
import { TicketStatus, Category, CTIType, Item } from '../types';
|
||||
import { useTickets, useCategories, useTypes, useItems } from '../api/queries';
|
||||
|
||||
const STATUSES: { value: TicketStatus | ''; label: string }[] = [
|
||||
{ value: '', label: 'All Statuses' },
|
||||
@@ -20,7 +20,6 @@ const STATUSES: { value: TicketStatus | ''; label: string }[] = [
|
||||
const selectClass =
|
||||
'bg-gray-800 border border-gray-700 text-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
|
||||
|
||||
// Queue label built from whatever CTI level is selected
|
||||
function queueLabel(category: Category | null, type: CTIType | null, item: Item | null): string {
|
||||
if (item && type && category) return `${category.name} › ${type.name} › ${item.name}`;
|
||||
if (type && category) return `${category.name} › ${type.name}`;
|
||||
@@ -29,41 +28,44 @@ function queueLabel(category: Category | null, type: CTIType | null, item: Item
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const [status, setStatus] = useState<TicketStatus | ''>('');
|
||||
const [severity, setSeverity] = useState('');
|
||||
|
||||
// CTI queue filter state
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [types, setTypes] = useState<CTIType[]>([]);
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<CTIType | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||
const [showQueueFilter, setShowQueueFilter] = useState(false);
|
||||
|
||||
const { data: categories = [] } = useCategories();
|
||||
const { data: types = [] } = useTypes(selectedCategory?.id);
|
||||
const { data: items = [] } = useItems(selectedType?.id);
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Category[]>('/cti/categories').then((r) => setCategories(r.data));
|
||||
}, []);
|
||||
const t = setTimeout(() => setDebouncedSearch(search), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [search]);
|
||||
|
||||
const ticketParams: Record<string, string> = {};
|
||||
if (selectedItem) ticketParams.itemId = selectedItem.id;
|
||||
else if (selectedType) ticketParams.typeId = selectedType.id;
|
||||
else if (selectedCategory) ticketParams.categoryId = selectedCategory.id;
|
||||
if (status) ticketParams.status = status;
|
||||
if (severity) ticketParams.severity = severity;
|
||||
if (debouncedSearch) ticketParams.search = debouncedSearch;
|
||||
|
||||
const { data: tickets = [], isLoading } = useTickets(ticketParams);
|
||||
|
||||
const handleCategorySelect = (cat: Category) => {
|
||||
setSelectedCategory(cat);
|
||||
setSelectedType(null);
|
||||
setSelectedItem(null);
|
||||
setTypes([]);
|
||||
setItems([]);
|
||||
api
|
||||
.get<CTIType[]>('/cti/types', { params: { categoryId: cat.id } })
|
||||
.then((r) => setTypes(r.data));
|
||||
};
|
||||
|
||||
const handleTypeSelect = (type: CTIType) => {
|
||||
setSelectedType(type);
|
||||
setSelectedItem(null);
|
||||
setItems([]);
|
||||
api.get<Item[]>('/cti/items', { params: { typeId: type.id } }).then((r) => setItems(r.data));
|
||||
};
|
||||
|
||||
const handleItemSelect = (item: Item) => {
|
||||
@@ -75,34 +77,8 @@ export default function Dashboard() {
|
||||
setSelectedCategory(null);
|
||||
setSelectedType(null);
|
||||
setSelectedItem(null);
|
||||
setTypes([]);
|
||||
setItems([]);
|
||||
};
|
||||
|
||||
// Derive the most specific filter param
|
||||
const queueParams: Record<string, string> = {};
|
||||
if (selectedItem) queueParams.itemId = selectedItem.id;
|
||||
else if (selectedType) queueParams.typeId = selectedType.id;
|
||||
else if (selectedCategory) queueParams.categoryId = selectedCategory.id;
|
||||
|
||||
const fetchTickets = useCallback(() => {
|
||||
setLoading(true);
|
||||
const params: Record<string, string> = { ...queueParams };
|
||||
if (status) params.status = status;
|
||||
if (severity) params.severity = severity;
|
||||
if (search) params.search = search;
|
||||
api
|
||||
.get<Ticket[]>('/tickets', { params })
|
||||
.then((r) => setTickets(r.data))
|
||||
.finally(() => setLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status, severity, search, selectedCategory, selectedType, selectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(fetchTickets, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [fetchTickets]);
|
||||
|
||||
const activeQueue = queueLabel(selectedCategory, selectedType, selectedItem);
|
||||
|
||||
return (
|
||||
@@ -263,7 +239,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Ticket list */}
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
<div className="text-center py-16 text-gray-600 text-sm">Loading...</div>
|
||||
) : tickets.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-600 text-sm">No tickets found</div>
|
||||
|
||||
Reference in New Issue
Block a user