feat(tickets): popup réutilisable de sélection de ticket (#18)

Ajoute TicketPicker, popup modale réutilisable de sélection de ticket,
socle du chemin critique du sprint UI rework (#17 et #19 en dépendent).

- TicketPicker.tsx : popup de recherche/sélection réutilisable
- useTicketSearch.ts : hook de recherche/filtrage extrait et partagé
- TicketFacetsBar.tsx : barre de facettes extraite de TicketsPanel
  pour réutilisation (listing principal + picker)
- shared/ui/zIndex.ts : échelle z-index centralisée
- TicketsPanel : consomme la barre de facettes extraite

Tests : TicketPicker.test.tsx 8/8, suite tickets 37/37, tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 15:54:47 +02:00
parent d97abb116f
commit 140daae8b3
8 changed files with 954 additions and 65 deletions

View File

@ -14,14 +14,13 @@ import { Button, Input, Panel, Spinner, cn } from "@/shared";
import { useTickets } from "./useTickets";
import { useProjectAgents } from "./useProjectAgents";
import { SprintManager } from "./SprintManager";
import { TicketFacetsBar } from "./TicketFacetsBar";
import {
PriorityBadge,
StatusBadge,
TICKET_PRIORITIES,
TICKET_STATUSES,
TicketRef,
priorityLabel,
statusLabel,
} from "./ticketMeta";
const selectClass = cn(
@ -152,60 +151,19 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
{/* ── Filters ── */}
<div className="mb-3 flex flex-col gap-2">
<Input
aria-label="search tickets"
placeholder="Search title/description…"
value={vm.query.text ?? ""}
onChange={(e) =>
vm.setQuery({ ...vm.query, text: e.target.value || undefined })
{/* Shared search + status/priority facets (#18). Assignee stays here —
it needs the project agent roster and is out of the picker's scope. */}
<TicketFacetsBar
text={vm.query.text ?? ""}
onTextChange={(text) =>
vm.setQuery({ ...vm.query, text: text || undefined })
}
statuses={vm.query.statuses ?? []}
priorities={vm.query.priorities ?? []}
onToggleStatus={vm.toggleStatus}
onTogglePriority={vm.togglePriority}
onClearFacets={vm.clearFacets}
/>
{/* 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 assignee"
@ -225,17 +183,6 @@ 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>