feat(tickets): filtres multi-critères par cases à cocher (backend + frontend)

Passe les filtres de la liste des tickets en sélection multiple (#12).

Backend Rust :
- IssueListFilter : statuses/priorities en Vec, filter_matches en OR intra-champ
  et AND inter-champs
- TicketListRequestDto en tableaux + from_request (parse/déduplication)
- MCP idea_ticket_list aligné sur le nouveau contrat

Frontend :
- TicketListQuery.statuses/priorities
- UI de cases à cocher, toggle/clear des filtres

Tests verts : issue_store (7), app-tauri --lib (59), mcp_server (24),
issue_usecases (6), sprint_usecases (6), frontend vitest (505), npm run build (exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:41:01 +02:00
parent 413db25f08
commit feeca3462c
11 changed files with 410 additions and 81 deletions

View File

@ -20,6 +20,8 @@ import {
type Sprint,
type Ticket,
type TicketList,
type TicketPriority,
type TicketStatus,
} from "@/domain";
import type { CreateTicketInput, TicketListQuery } from "@/ports";
import { useGateways } from "@/app/di";
@ -32,6 +34,16 @@ export interface TicketsViewModel {
busy: boolean;
query: TicketListQuery;
setQuery: (next: TicketListQuery) => void;
/**
* Toggles a status in the multi-select filter (ticket #12): adds it when
* absent, removes it when present. An empty set ⇒ no status constraint (all).
* The list re-fetches automatically as the query changes.
*/
toggleStatus: (status: TicketStatus) => void;
/** Toggles a priority in the multi-select filter (ticket #12). */
togglePriority: (priority: TicketPriority) => void;
/** Clears both facet filters (status + priority), leaving other criteria. */
clearFacets: () => void;
refresh: () => Promise<void>;
create: (input: CreateTicketInput) => Promise<Ticket | null>;
/**
@ -89,6 +101,35 @@ export function useTickets(projectId: string): TicketsViewModel {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId, ticket, queryKey]);
// Toggle a value in one facet set, dropping the key entirely when it empties
// so the query stays clean ("no constraint"). Setting a fresh object identity
// re-fetches via the `queryKey` effect (ticket #12).
const toggleStatus = useCallback((status: TicketStatus) => {
setQuery((prev) => {
const current = prev.statuses ?? [];
const next = current.includes(status)
? current.filter((s) => s !== status)
: [...current, status];
const { statuses: _drop, ...rest } = prev;
return next.length > 0 ? { ...rest, statuses: next } : rest;
});
}, []);
const togglePriority = useCallback((priority: TicketPriority) => {
setQuery((prev) => {
const current = prev.priorities ?? [];
const next = current.includes(priority)
? current.filter((p) => p !== priority)
: [...current, priority];
const { priorities: _drop, ...rest } = prev;
return next.length > 0 ? { ...rest, priorities: next } : rest;
});
}, []);
const clearFacets = useCallback(() => {
setQuery(({ statuses: _s, priorities: _p, ...rest }) => rest);
}, []);
const refreshSprints = useCallback(async () => {
try {
setSprints(await ticket.listSprints(projectId));
@ -242,6 +283,9 @@ export function useTickets(projectId: string): TicketsViewModel {
busy,
query,
setQuery,
toggleStatus,
togglePriority,
clearFacets,
refresh,
create,
assignSprint,