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:
2026-07-06 06:41:01 +02:00
parent 413db25f08
commit feeca3462c
11 changed files with 410 additions and 81 deletions

View File

@ -231,13 +231,10 @@ async fn write_index(root: &ProjectPath, rows: &[IssueIndexEntry]) -> Result<(),
}
fn filter_matches(row: &IssueIndexEntry, filter: &IssueListFilter) -> bool {
if filter.status.is_some_and(|status| row.status != status) {
if !filter.statuses.is_empty() && !filter.statuses.contains(&row.status) {
return false;
}
if filter
.priority
.is_some_and(|priority| row.priority != priority)
{
if !filter.priorities.is_empty() && !filter.priorities.contains(&row.priority) {
return false;
}
if let Some(agent_id) = filter.assigned_agent_id {

View File

@ -125,8 +125,16 @@ pub fn catalogue() -> Vec<ToolDef> {
input_schema: json!({
"type": "object",
"properties": {
"status": status.clone(),
"priority": priority.clone(),
"statuses": {
"type": "array",
"items": status.clone(),
"uniqueItems": true
},
"priorities": {
"type": "array",
"items": priority.clone(),
"uniqueItems": true
},
"assignedAgentId": { "type": "string", "format": "uuid" },
"text": { "type": "string" },
"limit": { "type": "integer", "minimum": 1 },

View File

@ -112,7 +112,7 @@ async fn issue_store_update_checks_expected_version() {
}
#[tokio::test]
async fn issue_store_lists_by_index_filters() {
async fn issue_store_lists_by_index_filters_with_empty_or_and_and_semantics() {
let tmp = TempDir::new();
let root = tmp.root();
let store = FsIssueStore::new();
@ -127,21 +127,61 @@ async fn issue_store_lists_by_index_filters() {
})
.unwrap();
store.create(&root, &closed).await.unwrap();
let qa_high = issue(&root, 3, "Gamma")
.mutate(IssueActor::System, 3_000, |i| {
i.status = IssueStatus::Qa;
i.priority = IssuePriority::Critical;
})
.unwrap();
store.create(&root, &qa_high).await.unwrap();
let rows = store
let all = store.list(&root, IssueListFilter::default()).await.unwrap();
assert_eq!(all.len(), 3);
let by_status_or = store
.list(
&root,
IssueListFilter {
status: Some(IssueStatus::Closed),
priority: Some(IssuePriority::Low),
statuses: vec![IssueStatus::Closed, IssueStatus::Qa],
..IssueListFilter::default()
},
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].title, "Beta");
assert_eq!(by_status_or.len(), 2);
assert_eq!(by_status_or[0].title, "Beta");
assert_eq!(by_status_or[1].title, "Gamma");
let by_priority_or = store
.list(
&root,
IssueListFilter {
priorities: vec![IssuePriority::Low, IssuePriority::Critical],
..IssueListFilter::default()
},
)
.await
.unwrap();
assert_eq!(by_priority_or.len(), 2);
assert_eq!(by_priority_or[0].title, "Beta");
assert_eq!(by_priority_or[1].title, "Gamma");
let by_status_and_priority = store
.list(
&root,
IssueListFilter {
statuses: vec![IssueStatus::Closed, IssueStatus::Qa],
priorities: vec![IssuePriority::Low],
..IssueListFilter::default()
},
)
.await
.unwrap();
assert_eq!(by_status_and_priority.len(), 1);
assert_eq!(by_status_and_priority[0].title, "Beta");
}
#[tokio::test]