bulk operations fixes
- Frontend: implémente bulkDelete, bulkArchive, bulkRestore - Backend: adapts ticket DTOs pour bulk operations - Infrastructure: MCP server pour gestion bulk tickets - Tests: coverage frontend + backend
This commit is contained in:
@ -280,6 +280,7 @@ pub fn run() {
|
||||
tickets::ticket_delete,
|
||||
tickets::ticket_bulk_update_status,
|
||||
tickets::ticket_bulk_update_priority,
|
||||
tickets::ticket_bulk_delete,
|
||||
tickets::open_ticket_chat,
|
||||
tickets::close_ticket_chat,
|
||||
tickets::ticket_list,
|
||||
|
||||
@ -344,7 +344,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
|
||||
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
|
||||
"idea_ticket_bulk_update_status"
|
||||
| "idea_ticket_bulk_update_priority"
|
||||
| "idea_ticket_bulk_delete"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@ impl AppState {
|
||||
update: Arc::clone(&core.update_issue),
|
||||
bulk_update_status: Arc::clone(&core.bulk_update_issue_status),
|
||||
bulk_update_priority: Arc::clone(&core.bulk_update_issue_priority),
|
||||
bulk_delete: Arc::clone(&core.bulk_delete_issues),
|
||||
read_carnet: Arc::clone(&core.read_issue_carnet),
|
||||
update_carnet: Arc::clone(&core.update_issue_carnet),
|
||||
link: Arc::clone(&core.link_issues),
|
||||
|
||||
@ -31,6 +31,7 @@ pub struct AppTicketToolProvider {
|
||||
pub update: Arc<application::UpdateIssue>,
|
||||
pub bulk_update_status: Arc<application::BulkUpdateIssueStatus>,
|
||||
pub bulk_update_priority: Arc<application::BulkUpdateIssuePriority>,
|
||||
pub bulk_delete: Arc<application::BulkDeleteIssues>,
|
||||
pub read_carnet: Arc<application::ReadIssueCarnet>,
|
||||
pub update_carnet: Arc<application::UpdateIssueCarnet>,
|
||||
pub link: Arc<application::LinkIssues>,
|
||||
@ -204,6 +205,14 @@ impl TicketToolProvider for AppTicketToolProvider {
|
||||
.await;
|
||||
json!(TicketBulkResultDto::from(out))
|
||||
}
|
||||
"idea_ticket_bulk_delete" => {
|
||||
let req = mcp_bulk_delete_request(project, arguments)?;
|
||||
let out = self
|
||||
.bulk_delete
|
||||
.execute(bulk_delete_input(project.clone(), req).map_err(dto_tool_error)?)
|
||||
.await;
|
||||
json!(TicketBulkResultDto::from(out))
|
||||
}
|
||||
"idea_ticket_read_carnet" => {
|
||||
let carnet = self
|
||||
.read_carnet
|
||||
@ -390,6 +399,19 @@ pub async fn ticket_bulk_update_priority(
|
||||
Ok(TicketBulkResultDto::from(out))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ticket_bulk_delete(
|
||||
request: TicketBulkDeleteRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<TicketBulkResultDto, ErrorDto> {
|
||||
let project = resolve_project(&state, &request.project_id).await?;
|
||||
let out = state
|
||||
.bulk_delete_issues
|
||||
.execute(bulk_delete_input(project, request)?)
|
||||
.await;
|
||||
Ok(TicketBulkResultDto::from(out))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_ticket_chat(
|
||||
request: OpenTicketChatRequestDto,
|
||||
@ -844,6 +866,16 @@ fn mcp_bulk_update_priority_request(
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
fn mcp_bulk_delete_request(
|
||||
project: &Project,
|
||||
arguments: Value,
|
||||
) -> Result<TicketBulkDeleteRequestDto, TicketToolError> {
|
||||
let mut req: TicketBulkDeleteRequestDto = serde_json::from_value(arguments)
|
||||
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
|
||||
req.project_id = project.id.to_string();
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
fn parse_json_ref(arguments: &Value, key: &str) -> Result<IssueRef, TicketToolError> {
|
||||
required_str(arguments, key).and_then(|raw| parse_ref_dto(raw).map_err(dto_tool_error))
|
||||
}
|
||||
|
||||
@ -14,26 +14,26 @@ use std::sync::{Arc, Mutex};
|
||||
use application::{
|
||||
AgentResumer, AgentWakeService, AppError, AssignIssueAgent, AssignSkillToAgent,
|
||||
AssignTicketToSprint, AttachLiveAgent, AuthenticateSession, BackgroundCommandArchive,
|
||||
BulkUpdateIssuePriority, BulkUpdateIssueStatus, CancelBackgroundTask, ChangeAgentProfile,
|
||||
CheckEmbedderSuggestion, CloneOpenCodeProfileFromSeed, CloneProfileFromSeed, CloseProject,
|
||||
CloseTab, CloseTerminal, CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases,
|
||||
CreateAgentFromScratch, CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory,
|
||||
CreateProject, CreateSkill, CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile,
|
||||
DeleteIssue, DeleteLayout, DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile,
|
||||
DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
|
||||
DetectProfiles, DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState,
|
||||
GetAppExitWorkGuardState, GetLiveStateLean, GetMemory, GetProjectPermissions,
|
||||
GetProjectSystemPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
|
||||
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
|
||||
HealthUseCase, InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
|
||||
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
|
||||
ListAgentsInput, ListClaudeModels, ListCodexModels, ListDevices, ListEmbedderProfiles,
|
||||
ListIssues, ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
|
||||
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
|
||||
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
|
||||
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue,
|
||||
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||
BulkDeleteIssues, BulkUpdateIssuePriority, BulkUpdateIssueStatus, CancelBackgroundTask,
|
||||
ChangeAgentProfile, CheckEmbedderSuggestion, CloneOpenCodeProfileFromSeed,
|
||||
CloneProfileFromSeed, CloseProject, CloseTab, CloseTerminal, CloseTicketAssistant,
|
||||
ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch, CreateAgentFromTemplate,
|
||||
CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateSprint,
|
||||
CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout, DeleteMemory,
|
||||
DeleteModelArtifact, DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint,
|
||||
DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
|
||||
DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState,
|
||||
GetLiveStateLean, GetMemory, GetProjectPermissions, GetProjectSystemPermissions,
|
||||
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
|
||||
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation,
|
||||
InstallPluginFromArchive, InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent,
|
||||
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels,
|
||||
ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
|
||||
ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
|
||||
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
|
||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
|
||||
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
|
||||
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
||||
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
|
||||
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||
@ -991,6 +991,8 @@ pub struct BackendCore {
|
||||
pub bulk_update_issue_status: Arc<BulkUpdateIssueStatus>,
|
||||
/// Update public ticket priorities in batch.
|
||||
pub bulk_update_issue_priority: Arc<BulkUpdateIssuePriority>,
|
||||
/// Delete public tickets in batch.
|
||||
pub bulk_delete_issues: Arc<BulkDeleteIssues>,
|
||||
/// Read a ticket carnet.
|
||||
pub read_issue_carnet: Arc<ReadIssueCarnet>,
|
||||
/// Update a ticket carnet.
|
||||
@ -1684,6 +1686,10 @@ impl BackendCore {
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let bulk_delete_issues = Arc::new(BulkDeleteIssues::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let read_issue_carnet = Arc::new(ReadIssueCarnet::new(Arc::clone(&issue_store_port)));
|
||||
let update_issue_carnet = Arc::new(UpdateIssueCarnet::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
@ -2834,6 +2840,7 @@ impl BackendCore {
|
||||
update_issue,
|
||||
bulk_update_issue_status,
|
||||
bulk_update_issue_priority,
|
||||
bulk_delete_issues,
|
||||
read_issue_carnet,
|
||||
update_issue_carnet,
|
||||
link_issues,
|
||||
@ -4967,6 +4974,7 @@ mod mcp_serve_peer_tests {
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_bulk_delete",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
@ -4987,8 +4995,8 @@ mod mcp_serve_peer_tests {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
33,
|
||||
"exactly the thirty-three exposed idea_* tools; got {names:?}"
|
||||
34,
|
||||
"exactly the thirty-four exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
drop(client); // EOF ⇒ serve loop ends
|
||||
|
||||
@ -357,7 +357,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
|
||||
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
|
||||
"idea_ticket_bulk_update_status"
|
||||
| "idea_ticket_bulk_update_priority"
|
||||
| "idea_ticket_bulk_delete"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
BulkIssueMutationOutput, BulkUpdateIssuePriorityInput, BulkUpdateIssueStatusInput,
|
||||
CreateIssueInput, UpdateIssueInput,
|
||||
BulkDeleteIssuesInput, BulkIssueMutationOutput, BulkUpdateIssuePriorityInput,
|
||||
BulkUpdateIssueStatusInput, CreateIssueInput, UpdateIssueInput,
|
||||
};
|
||||
use domain::{
|
||||
AgentId, AgentIssueRole, Issue, IssueActor, IssueCarnet, IssueIndexEntry, IssueLink,
|
||||
@ -225,6 +225,15 @@ pub struct TicketBulkUpdatePriorityRequestDto {
|
||||
pub priority: String,
|
||||
}
|
||||
|
||||
/// Bulk delete request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketBulkDeleteRequestDto {
|
||||
#[serde(default)]
|
||||
pub project_id: String,
|
||||
pub refs: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bulk operation response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -757,6 +766,16 @@ pub fn bulk_update_priority_input(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bulk_delete_input(
|
||||
project: Project,
|
||||
request: TicketBulkDeleteRequestDto,
|
||||
) -> Result<BulkDeleteIssuesInput, ErrorDto> {
|
||||
Ok(BulkDeleteIssuesInput {
|
||||
project,
|
||||
issue_refs: parse_bulk_refs(request.refs)?,
|
||||
})
|
||||
}
|
||||
|
||||
impl From<BulkIssueMutationOutput> for TicketBulkResultDto {
|
||||
fn from(output: BulkIssueMutationOutput) -> Self {
|
||||
Self {
|
||||
@ -1272,6 +1291,27 @@ mod tests {
|
||||
assert!(err.message.contains("duplicate ticket ref: #1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulk_delete_input_maps_refs_in_order() {
|
||||
let input = bulk_delete_input(
|
||||
project_for_tests(),
|
||||
TicketBulkDeleteRequestDto {
|
||||
project_id: String::new(),
|
||||
refs: vec!["#2".into(), "#1".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
input
|
||||
.issue_refs
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["#2", "#1"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actor_from_requester_maps_agent_uuid_else_user() {
|
||||
let agent_id = AgentId::new_random();
|
||||
|
||||
@ -919,7 +919,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
|
||||
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
|
||||
"idea_ticket_bulk_update_status"
|
||||
| "idea_ticket_bulk_update_priority"
|
||||
| "idea_ticket_bulk_delete"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ pub fn is_ticket_tool(name: &str) -> bool {
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_bulk_update_status"
|
||||
| "idea_ticket_bulk_update_priority"
|
||||
| "idea_ticket_bulk_delete"
|
||||
| "idea_ticket_read_carnet"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_link"
|
||||
@ -255,6 +256,18 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_bulk_delete",
|
||||
description: "Delete several IdeA tickets and return one result per ticket.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refs": refs.clone()
|
||||
},
|
||||
"required": ["refs"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_read_carnet",
|
||||
description: "Read the editable carnet attached to a ticket.",
|
||||
|
||||
@ -76,6 +76,7 @@ pub const WRITE_ACTION_TOOLS: &[&str] = &[
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_bulk_delete",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
|
||||
@ -751,6 +751,7 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_bulk_delete",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
@ -771,8 +772,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
33,
|
||||
"exactly the thirty-three exposed idea_* tools; got {names:?}"
|
||||
34,
|
||||
"exactly the thirty-four exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
@ -1371,13 +1372,14 @@ async fn bounded_ticket_mutation_tools_reject_other_issue_before_ticket_provider
|
||||
"idea_ticket_bulk_update_priority",
|
||||
json!({ "refs": ["#8"], "priority": "high" }),
|
||||
),
|
||||
(25, "idea_ticket_bulk_delete", json!({ "refs": ["#8"] })),
|
||||
(
|
||||
25,
|
||||
26,
|
||||
"idea_ticket_link",
|
||||
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
(
|
||||
26,
|
||||
27,
|
||||
"idea_ticket_unlink",
|
||||
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
@ -1442,13 +1444,14 @@ async fn bounded_ticket_mutation_tools_allow_bound_issue_to_reach_ticket_provide
|
||||
"idea_ticket_bulk_update_priority",
|
||||
json!({ "refs": ["#7"], "priority": "high" }),
|
||||
),
|
||||
(35, "idea_ticket_bulk_delete", json!({ "refs": ["#7"] })),
|
||||
(
|
||||
35,
|
||||
36,
|
||||
"idea_ticket_link",
|
||||
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
(
|
||||
36,
|
||||
37,
|
||||
"idea_ticket_unlink",
|
||||
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user