feat(tickets): persistance des filtres tickets entre redémarrages (#29)

Introduit le port UiPreferencesGateway et son adapter uiPreferences,
avec le module ticketFilterPersistence qui sauvegarde/restaure les
filtres (recherche, statut, sprint, agents) via useTickets,
useTicketSearch et useProjectAgents. Couvert par tests unitaires et
un test d'intégration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 07:45:50 +02:00
parent 9ee7290cde
commit 79f06c26d9
13 changed files with 793 additions and 20 deletions

View File

@ -11,7 +11,7 @@
* or out of a sprint. Sprint lifecycle events refresh the sprint list.
*/
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
isSprintEvent,
@ -25,6 +25,10 @@ import {
} from "@/domain";
import type { CreateTicketInput, TicketListQuery } from "@/ports";
import { useGateways } from "@/app/di";
import {
hydrateListFilters,
persistListFilters,
} from "./ticketFilterPersistence";
export interface TicketsViewModel {
list: TicketList | null;
@ -76,12 +80,33 @@ function describe(e: unknown): string {
}
export function useTickets(projectId: string): TicketsViewModel {
const { ticket, system } = useGateways();
const { ticket, system, uiPreferences } = useGateways();
const [list, setList] = useState<TicketList | null>(null);
const [sprints, setSprints] = useState<Sprint[]>([]);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [query, setQuery] = useState<TicketListQuery>({});
// Hydrate the filters persisted for this project (ticket #29) on first render
// so reopening the window restores them without a flash of defaults. The
// assignee, if any, is validated for shape here and reconciled against the
// live agent roster by the panel (an obsolete assignee is then cleared).
const [query, setQuery] = useState<TicketListQuery>(() =>
hydrateListFilters(uiPreferences, projectId),
);
// Re-hydrate when the hosting project changes (the mounted hook is normally
// pinned to one project, but stay correct if `projectId` is swapped in place).
const hydratedProject = useRef(projectId);
useEffect(() => {
if (hydratedProject.current === projectId) return;
hydratedProject.current = projectId;
setQuery(hydrateListFilters(uiPreferences, projectId));
}, [projectId, uiPreferences]);
// Persist the filters (minus the opaque cursor) whenever they change, scoped
// to this project's list key.
useEffect(() => {
persistListFilters(uiPreferences, projectId, query);
}, [uiPreferences, projectId, query]);
// Stable dependency key so `refresh` only changes when a filter actually does.
const queryKey = useMemo(() => JSON.stringify(query), [query]);