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:
@ -78,7 +78,7 @@ describe("MockTicketGateway", () => {
|
||||
expect(events).toContain("issueCreated");
|
||||
});
|
||||
|
||||
it("filters by status and searches text", async () => {
|
||||
it("filters by statuses (OR within facet) and searches text (#12)", async () => {
|
||||
await ticket.create(PROJECT_ID, { title: "alpha bug", status: "open" });
|
||||
const t2 = await ticket.create(PROJECT_ID, { title: "beta task" });
|
||||
await ticket.update(PROJECT_ID, t2.ref, {
|
||||
@ -86,13 +86,55 @@ describe("MockTicketGateway", () => {
|
||||
expectedVersion: t2.version,
|
||||
});
|
||||
|
||||
const open = await ticket.list(PROJECT_ID, { status: "open" });
|
||||
const open = await ticket.list(PROJECT_ID, { statuses: ["open"] });
|
||||
expect(open.items.map((i) => i.ref)).toEqual(["#1"]);
|
||||
|
||||
// OR within the status facet: both values pass.
|
||||
const either = await ticket.list(PROJECT_ID, {
|
||||
statuses: ["open", "closed"],
|
||||
});
|
||||
expect(either.items.map((i) => i.ref).sort()).toEqual(["#1", "#2"]);
|
||||
|
||||
// An empty set ⇒ no status constraint (all pass).
|
||||
const all = await ticket.list(PROJECT_ID, { statuses: [] });
|
||||
expect(all.items).toHaveLength(2);
|
||||
|
||||
const search = await ticket.list(PROJECT_ID, { text: "beta" });
|
||||
expect(search.items.map((i) => i.ref)).toEqual(["#2"]);
|
||||
});
|
||||
|
||||
it("combines status and priority facets with AND (#12)", async () => {
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "a",
|
||||
status: "open",
|
||||
priority: "high",
|
||||
});
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "b",
|
||||
status: "open",
|
||||
priority: "low",
|
||||
});
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "c",
|
||||
status: "closed",
|
||||
priority: "high",
|
||||
});
|
||||
|
||||
// statuses:[open] AND priorities:[high] ⇒ only #1.
|
||||
const both = await ticket.list(PROJECT_ID, {
|
||||
statuses: ["open"],
|
||||
priorities: ["high"],
|
||||
});
|
||||
expect(both.items.map((i) => i.ref)).toEqual(["#1"]);
|
||||
|
||||
// OR within status (open|closed) AND priorities:[high] ⇒ #1 and #3.
|
||||
const mix = await ticket.list(PROJECT_ID, {
|
||||
statuses: ["open", "closed"],
|
||||
priorities: ["high"],
|
||||
});
|
||||
expect(mix.items.map((i) => i.ref).sort()).toEqual(["#1", "#3"]);
|
||||
});
|
||||
|
||||
it("rejects a stale write with a version conflict", async () => {
|
||||
const t = await ticket.create(PROJECT_ID, { title: "x" });
|
||||
await ticket.update(PROJECT_ID, t.ref, {
|
||||
@ -172,6 +214,59 @@ describe("TicketsView", () => {
|
||||
expect(await screen.findByText("Live created")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("multi-selects status/priority facets (OR intra, AND inter) and clears (#12)", async () => {
|
||||
const listSpy = vi.spyOn(ticket, "list");
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "Alpha",
|
||||
status: "open",
|
||||
priority: "high",
|
||||
});
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "Bravo",
|
||||
status: "closed",
|
||||
priority: "low",
|
||||
});
|
||||
await ticket.create(PROJECT_ID, {
|
||||
title: "Charlie",
|
||||
status: "QA",
|
||||
priority: "high",
|
||||
});
|
||||
|
||||
renderView(ticket, system, agent);
|
||||
|
||||
// All three visible initially (no facet ⇒ all pass).
|
||||
await screen.findByText("Alpha");
|
||||
expect(screen.getByText("Bravo")).toBeTruthy();
|
||||
expect(screen.getByText("Charlie")).toBeTruthy();
|
||||
|
||||
// Tick two statuses → OR within the facet ⇒ open|closed ⇒ Alpha + Bravo,
|
||||
// Charlie (QA) drops out.
|
||||
fireEvent.click(screen.getByLabelText("filter status Open"));
|
||||
fireEvent.click(screen.getByLabelText("filter status Closed"));
|
||||
await waitFor(() => expect(screen.queryByText("Charlie")).toBeNull());
|
||||
expect(screen.getByText("Alpha")).toBeTruthy();
|
||||
expect(screen.getByText("Bravo")).toBeTruthy();
|
||||
|
||||
// Add a priority → AND across facets ⇒ (open|closed) AND high ⇒ only Alpha.
|
||||
fireEvent.click(screen.getByLabelText("filter priority High"));
|
||||
await waitFor(() => expect(screen.queryByText("Bravo")).toBeNull());
|
||||
expect(screen.getByText("Alpha")).toBeTruthy();
|
||||
|
||||
// The gateway received the expected multi-select query.
|
||||
await waitFor(() =>
|
||||
expect(listSpy.mock.calls.at(-1)?.[1]).toMatchObject({
|
||||
statuses: ["open", "closed"],
|
||||
priorities: ["high"],
|
||||
}),
|
||||
);
|
||||
|
||||
// « Effacer » empties both facets ⇒ all three back.
|
||||
fireEvent.click(screen.getByLabelText("clear filters"));
|
||||
await waitFor(() => expect(screen.getByText("Charlie")).toBeTruthy());
|
||||
expect(screen.getByText("Bravo")).toBeTruthy();
|
||||
expect(screen.getByText("Alpha")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens the detail and edits status with a version bump", async () => {
|
||||
const t = await ticket.create(PROJECT_ID, { title: "Editable" });
|
||||
renderView(ticket, system, agent);
|
||||
|
||||
Reference in New Issue
Block a user