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:
@ -9,12 +9,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type {
|
||||
Sprint,
|
||||
TicketPriority,
|
||||
TicketStatus,
|
||||
TicketSummary,
|
||||
} from "@/domain";
|
||||
import type { Sprint, TicketPriority, TicketSummary } from "@/domain";
|
||||
import { Button, Input, Panel, Spinner, cn } from "@/shared";
|
||||
import { useTickets } from "./useTickets";
|
||||
import { useProjectAgents } from "./useProjectAgents";
|
||||
@ -165,45 +160,53 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
|
||||
vm.setQuery({ ...vm.query, text: e.target.value || undefined })
|
||||
}
|
||||
/>
|
||||
{/* Multi-select facets (ticket #12): OR within a facet, AND across; no
|
||||
box ticked ⇒ that facet is unconstrained. */}
|
||||
<fieldset
|
||||
aria-label="filter by status"
|
||||
className="flex flex-wrap items-center gap-x-3 gap-y-1"
|
||||
>
|
||||
<legend className="mr-1 text-xs font-medium text-muted">Statut</legend>
|
||||
{TICKET_STATUSES.map((s) => (
|
||||
<label
|
||||
key={s}
|
||||
className="flex cursor-pointer items-center gap-1.5 text-xs"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`filter status ${statusLabel(s)}`}
|
||||
className="accent-primary"
|
||||
checked={(vm.query.statuses ?? []).includes(s)}
|
||||
onChange={() => vm.toggleStatus(s)}
|
||||
/>
|
||||
<StatusBadge status={s} />
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
<fieldset
|
||||
aria-label="filter by priority"
|
||||
className="flex flex-wrap items-center gap-x-3 gap-y-1"
|
||||
>
|
||||
<legend className="mr-1 text-xs font-medium text-muted">
|
||||
Priorité
|
||||
</legend>
|
||||
{TICKET_PRIORITIES.map((p) => (
|
||||
<label
|
||||
key={p}
|
||||
className="flex cursor-pointer items-center gap-1.5 text-xs"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`filter priority ${priorityLabel(p)}`}
|
||||
className="accent-primary"
|
||||
checked={(vm.query.priorities ?? []).includes(p)}
|
||||
onChange={() => vm.togglePriority(p)}
|
||||
/>
|
||||
<PriorityBadge priority={p} />
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
aria-label="filter by status"
|
||||
className={selectClass}
|
||||
value={vm.query.status ?? ""}
|
||||
onChange={(e) =>
|
||||
vm.setQuery({
|
||||
...vm.query,
|
||||
status: (e.target.value || undefined) as TicketStatus | undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
{TICKET_STATUSES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{statusLabel(s)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
aria-label="filter by priority"
|
||||
className={selectClass}
|
||||
value={vm.query.priority ?? ""}
|
||||
onChange={(e) =>
|
||||
vm.setQuery({
|
||||
...vm.query,
|
||||
priority: (e.target.value || undefined) as
|
||||
| TicketPriority
|
||||
| undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">All priorities</option>
|
||||
{TICKET_PRIORITIES.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{priorityLabel(p)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
aria-label="filter by assignee"
|
||||
className={selectClass}
|
||||
@ -222,6 +225,17 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{((vm.query.statuses?.length ?? 0) > 0 ||
|
||||
(vm.query.priorities?.length ?? 0) > 0) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="clear filters"
|
||||
onClick={() => vm.clearFacets()}
|
||||
>
|
||||
Effacer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user