Files
IdeA/frontend/src/features/tickets/TicketFacetsBar.tsx
Blomios 140daae8b3 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>
2026-07-06 15:54:47 +02:00

127 lines
4.1 KiB
TypeScript

/**
* Reusable ticket filter/search bar (UI rework #18): a free-text search input
* plus the multi-select status/priority checkbox facets. Extracted from
* {@link TicketsPanel} so both the main listing and the {@link TicketPicker}
* popup share one implementation and stay in sync.
*
* Purely controlled/presentational — it holds no state and knows nothing about
* the gateway; the owner (a view-model or {@link useTicketSearch}) drives every
* value and toggle. Facet semantics (ticket #12): OR within a facet, AND across;
* an empty set ⇒ that facet is unconstrained.
*
* The assignee filter stays with the main listing (it needs the project's agent
* roster and is out of the picker's scope), so it is intentionally not here.
*/
import type { TicketPriority, TicketStatus } from "@/domain";
import { Button, Input } from "@/shared";
import {
PriorityBadge,
StatusBadge,
TICKET_PRIORITIES,
TICKET_STATUSES,
priorityLabel,
statusLabel,
} from "./ticketMeta";
export interface TicketFacetsBarProps {
/** Current free-text search value (controlled). */
text: string;
/** Emitted on every keystroke with the raw input value. */
onTextChange: (text: string) => void;
/** Currently-selected status facet values. */
statuses: TicketStatus[];
/** Currently-selected priority facet values. */
priorities: TicketPriority[];
onToggleStatus: (status: TicketStatus) => void;
onTogglePriority: (priority: TicketPriority) => void;
/**
* Clears both facets. When provided, a « Effacer » button appears while any
* facet is active. Omit to hide the affordance (the caller clears elsewhere).
*/
onClearFacets?: () => void;
/** Placeholder for the search input. */
searchPlaceholder?: string;
}
export function TicketFacetsBar({
text,
onTextChange,
statuses,
priorities,
onToggleStatus,
onTogglePriority,
onClearFacets,
searchPlaceholder = "Search title/description…",
}: TicketFacetsBarProps) {
const hasFacets = statuses.length > 0 || priorities.length > 0;
return (
<div className="flex flex-col gap-2">
<Input
aria-label="search tickets"
placeholder={searchPlaceholder}
value={text}
onChange={(e) => onTextChange(e.target.value)}
/>
{/* 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={statuses.includes(s)}
onChange={() => onToggleStatus(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={priorities.includes(p)}
onChange={() => onTogglePriority(p)}
/>
<PriorityBadge priority={p} />
</label>
))}
</fieldset>
{onClearFacets && hasFacets && (
<div>
<Button
size="sm"
variant="ghost"
aria-label="clear filters"
onClick={onClearFacets}
>
Effacer
</Button>
</div>
)}
</div>
);
}