Ajoute created_by sur IssueIndexEntry/TicketSummaryDto + filtre createdBy (#5), et les use cases BulkUpdateIssueStatus/Priority + BulkDeleteIssues avec les DTO/outils MCP idea_ticket_bulk_* associés (#6). NON VERT : BulkDeleteIssues::execute référence BatchIssueResult::deleted(), constructeur pas encore ajouté à BatchIssueResult (seuls ok/err existent) -> `cargo check` échoue (E0599 dans application::issues::mod). Reste à DevBackend avant toute QA/merge. Branché sur feature/sdk-integration (dépend du commit #2 dans orchestrator/mcp/server.rs et tools.rs) ; à rebaser sur develop une fois feature/sdk-integration mergé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
865 lines
28 KiB
Rust
865 lines
28 KiB
Rust
//! Public ticket adapters (Tauri commands + MCP provider).
|
|
//!
|
|
//! Public wire names use `ticket`; the application/domain model remains `Issue`.
|
|
|
|
#![allow(missing_docs)]
|
|
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
use serde_json::{json, Value};
|
|
use tauri::State;
|
|
|
|
use application::{
|
|
AppError, AssignIssueAgentInput, AssignTicketToSprintInput, CloseTicketAssistantInput,
|
|
CreateSprintInput, DeleteIssueInput, DeleteSprintInput, LinkIssuesInput, ListIssuesInput,
|
|
ListSprintsInput, OpenProjectInput, OpenTicketAssistantInput, ReadIssueCarnetInput,
|
|
ReadIssueInput, RenameSprintInput, ReorderSprintsInput, UnassignTicketFromSprintInput,
|
|
UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
|
|
};
|
|
use domain::{IssueActor, IssueRef, Project};
|
|
use infrastructure::{TicketToolError, TicketToolProvider};
|
|
|
|
use crate::dto::*;
|
|
use crate::state::AppState;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppTicketToolProvider {
|
|
pub create: Arc<application::CreateIssue>,
|
|
pub read: Arc<application::ReadIssue>,
|
|
pub list: Arc<application::ListIssues>,
|
|
pub update: Arc<application::UpdateIssue>,
|
|
pub bulk_update_status: Arc<application::BulkUpdateIssueStatus>,
|
|
pub bulk_update_priority: Arc<application::BulkUpdateIssuePriority>,
|
|
pub read_carnet: Arc<application::ReadIssueCarnet>,
|
|
pub update_carnet: Arc<application::UpdateIssueCarnet>,
|
|
pub link: Arc<application::LinkIssues>,
|
|
pub unlink: Arc<application::UnlinkIssues>,
|
|
pub list_sprints: Arc<application::ListSprints>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl TicketToolProvider for AppTicketToolProvider {
|
|
async fn handle_ticket_tool(
|
|
&self,
|
|
project: &Project,
|
|
requester: &str,
|
|
name: &str,
|
|
arguments: Value,
|
|
) -> Result<Value, TicketToolError> {
|
|
let actor = actor_from_requester(requester);
|
|
let result = match name {
|
|
"idea_ticket_create" => {
|
|
let req = mcp_create_request(project, arguments)?;
|
|
let issue = self
|
|
.create
|
|
.execute(create_input(project.clone(), req, actor).map_err(dto_tool_error)?)
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
"idea_ticket_read" => {
|
|
let issue_ref = parse_json_ref(&arguments, "ref")?;
|
|
let include_carnet = arguments
|
|
.get("includeCarnet")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false);
|
|
let issue = self
|
|
.read
|
|
.execute(ReadIssueInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
let fallback_carnet = issue.carnet.as_str().to_owned();
|
|
let carnet = if include_carnet {
|
|
Some(
|
|
read_ticket_carnet_body_best_effort(
|
|
&*self.read_carnet,
|
|
project,
|
|
issue_ref,
|
|
fallback_carnet,
|
|
)
|
|
.await
|
|
.map_err(ticket_error)?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
let sprints = self
|
|
.list_sprints
|
|
.execute(ListSprintsInput {
|
|
project: project.clone(),
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.sprints;
|
|
json!(TicketDto::from_issue_with_sprints(issue, carnet, &sprints))
|
|
}
|
|
"idea_ticket_list" => {
|
|
let req = mcp_list_request(project, arguments)?;
|
|
let mut rows = self
|
|
.list
|
|
.execute(ListIssuesInput {
|
|
project: project.clone(),
|
|
filter: req.filter,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issues;
|
|
sort_ticket_rows(&mut rows, req.sort);
|
|
let sprints = self
|
|
.list_sprints
|
|
.execute(ListSprintsInput {
|
|
project: project.clone(),
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.sprints;
|
|
let page = paginate_with_sprints(rows, req.limit, req.cursor, req.sort, &sprints)
|
|
.map_err(|e| TicketToolError::new("invalid", e.message))?;
|
|
json!(page)
|
|
}
|
|
"idea_sprint_list" => {
|
|
let rows = self
|
|
.list_sprints
|
|
.execute(ListSprintsInput {
|
|
project: project.clone(),
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.sprints;
|
|
json!(SprintListDto::from(rows))
|
|
}
|
|
"idea_ticket_update" => {
|
|
let req = mcp_update_request(project, arguments)?;
|
|
let issue = self
|
|
.update
|
|
.execute(update_input(project.clone(), req, actor).map_err(dto_tool_error)?)
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
"idea_ticket_update_status" => {
|
|
let issue = self
|
|
.update
|
|
.execute(UpdateIssueInput {
|
|
project: project.clone(),
|
|
issue_ref: parse_json_ref(&arguments, "ref")?,
|
|
expected_version: parse_json_version(&arguments)?,
|
|
title: None,
|
|
description: None,
|
|
status: Some(parse_status(required_str(&arguments, "status")?)?),
|
|
priority: None,
|
|
assigned_agent_ids: None,
|
|
actor,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
"idea_ticket_update_priority" => {
|
|
let issue = self
|
|
.update
|
|
.execute(UpdateIssueInput {
|
|
project: project.clone(),
|
|
issue_ref: parse_json_ref(&arguments, "ref")?,
|
|
expected_version: parse_json_version(&arguments)?,
|
|
title: None,
|
|
description: None,
|
|
status: None,
|
|
priority: Some(parse_priority(required_str(&arguments, "priority")?)?),
|
|
assigned_agent_ids: None,
|
|
actor,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
"idea_ticket_bulk_update_status" => {
|
|
let req = mcp_bulk_update_status_request(project, arguments)?;
|
|
let out = self
|
|
.bulk_update_status
|
|
.execute(
|
|
bulk_update_status_input(project.clone(), req, actor)
|
|
.map_err(dto_tool_error)?,
|
|
)
|
|
.await;
|
|
json!(TicketBulkResultDto::from(out))
|
|
}
|
|
"idea_ticket_bulk_update_priority" => {
|
|
let req = mcp_bulk_update_priority_request(project, arguments)?;
|
|
let out = self
|
|
.bulk_update_priority
|
|
.execute(
|
|
bulk_update_priority_input(project.clone(), req, actor)
|
|
.map_err(dto_tool_error)?,
|
|
)
|
|
.await;
|
|
json!(TicketBulkResultDto::from(out))
|
|
}
|
|
"idea_ticket_read_carnet" => {
|
|
let carnet = self
|
|
.read_carnet
|
|
.execute(ReadIssueCarnetInput {
|
|
project: project.clone(),
|
|
issue_ref: parse_json_ref(&arguments, "ref")?,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.carnet;
|
|
json!(TicketCarnetDto::from(carnet))
|
|
}
|
|
"idea_ticket_update_carnet" => {
|
|
let issue_ref = parse_json_ref(&arguments, "ref")?;
|
|
self.update_carnet
|
|
.execute(UpdateIssueCarnetInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
expected_version: parse_json_version(&arguments)?,
|
|
carnet: required_str(&arguments, "carnet")?.to_owned(),
|
|
actor,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?;
|
|
let issue = self
|
|
.read
|
|
.execute(ReadIssueInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(
|
|
issue,
|
|
Some(read_carnet_body(&*self.read_carnet, project, issue_ref).await?)
|
|
))
|
|
}
|
|
"idea_ticket_link" => {
|
|
let issue = self
|
|
.link
|
|
.execute(LinkIssuesInput {
|
|
project: project.clone(),
|
|
issue_ref: parse_json_ref(&arguments, "ref")?,
|
|
target: parse_json_ref(&arguments, "targetRef")?,
|
|
kind: parse_link_kind(required_str(&arguments, "kind")?)?,
|
|
expected_version: parse_json_version(&arguments)?,
|
|
actor,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
"idea_ticket_unlink" => {
|
|
let kind = arguments
|
|
.get("kind")
|
|
.and_then(Value::as_str)
|
|
.map(parse_link_kind)
|
|
.transpose()?;
|
|
let issue = self
|
|
.unlink
|
|
.execute(UnlinkIssuesInput {
|
|
project: project.clone(),
|
|
issue_ref: parse_json_ref(&arguments, "ref")?,
|
|
target: parse_json_ref(&arguments, "targetRef")?,
|
|
kind,
|
|
expected_version: parse_json_version(&arguments)?,
|
|
actor,
|
|
})
|
|
.await
|
|
.map_err(ticket_error)?
|
|
.issue;
|
|
json!(TicketDto::from_issue(issue, None))
|
|
}
|
|
_ => {
|
|
return Err(TicketToolError::new(
|
|
"unknownTool",
|
|
format!("unknown tool: {name}"),
|
|
))
|
|
}
|
|
};
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_create(
|
|
request: TicketCreateRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.create_issue
|
|
.execute(create_input(project, request, IssueActor::User)?)
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_read(
|
|
request: TicketReadRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue_ref = parse_ref_dto(&request.r#ref)?;
|
|
let issue = state
|
|
.read_issue
|
|
.execute(ReadIssueInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
let fallback_carnet = issue.carnet.as_str().to_owned();
|
|
let carnet = if request.include_carnet.unwrap_or(false) {
|
|
Some(
|
|
read_ticket_carnet_body_best_effort(
|
|
&state.read_issue_carnet,
|
|
&project,
|
|
issue_ref,
|
|
fallback_carnet,
|
|
)
|
|
.await
|
|
.map_err(ErrorDto::from)?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(TicketDto::from_issue(issue, carnet))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_delete(
|
|
request: TicketDeleteRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
state
|
|
.delete_issue
|
|
.execute(DeleteIssueInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
})
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(ErrorDto::from)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_bulk_update_status(
|
|
request: TicketBulkUpdateStatusRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketBulkResultDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let out = state
|
|
.bulk_update_issue_status
|
|
.execute(bulk_update_status_input(
|
|
project,
|
|
request,
|
|
IssueActor::User,
|
|
)?)
|
|
.await;
|
|
Ok(TicketBulkResultDto::from(out))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_bulk_update_priority(
|
|
request: TicketBulkUpdatePriorityRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketBulkResultDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let out = state
|
|
.bulk_update_issue_priority
|
|
.execute(bulk_update_priority_input(
|
|
project,
|
|
request,
|
|
IssueActor::User,
|
|
)?)
|
|
.await;
|
|
Ok(TicketBulkResultDto::from(out))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn open_ticket_chat(
|
|
request: OpenTicketChatRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketChatDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let output = state
|
|
.open_ticket_assistant
|
|
.execute(OpenTicketAssistantInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.issue_ref)?,
|
|
profile_id: parse_profile_id_dto(&request.profile_id)?,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?;
|
|
Ok(TicketChatDto {
|
|
session_id: output.session_id.to_string(),
|
|
requester: output.requester,
|
|
issue_ref: output.issue_ref.to_string(),
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn close_ticket_chat(
|
|
request: CloseTicketChatRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), ErrorDto> {
|
|
let _project = resolve_project(&state, &request.project_id).await?;
|
|
state
|
|
.close_ticket_assistant
|
|
.execute(CloseTicketAssistantInput {
|
|
issue_ref: parse_ref_dto(&request.issue_ref)?,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_list(
|
|
request: TicketListRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketListDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let page = TicketListPageInput::from_request(request)?;
|
|
let mut rows = state
|
|
.list_issues
|
|
.execute(ListIssuesInput {
|
|
project,
|
|
filter: page.filter,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issues;
|
|
sort_ticket_rows(&mut rows, page.sort);
|
|
paginate(rows, page.limit, page.cursor, page.sort)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sprint_create(
|
|
request: SprintCreateRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<SprintDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let sprint = state
|
|
.create_sprint
|
|
.execute(CreateSprintInput {
|
|
project,
|
|
name: request.name,
|
|
status: request
|
|
.status
|
|
.as_deref()
|
|
.map(parse_sprint_status_dto)
|
|
.transpose()?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.sprint;
|
|
Ok(SprintDto::from_sprint(sprint, 0))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sprint_list(
|
|
request: SprintListRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<SprintListDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let rows = state
|
|
.list_sprints
|
|
.execute(ListSprintsInput { project })
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.sprints;
|
|
Ok(SprintListDto::from(rows))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sprint_rename(
|
|
request: SprintRenameRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<SprintDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let sprint = state
|
|
.rename_sprint
|
|
.execute(RenameSprintInput {
|
|
project,
|
|
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
|
|
expected_version: sprint_version_dto(request.expected_version)?,
|
|
name: request.name,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.sprint;
|
|
Ok(SprintDto::from_sprint(sprint, 0))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sprint_reorder(
|
|
request: SprintReorderRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<SprintListDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
state
|
|
.reorder_sprints
|
|
.execute(ReorderSprintsInput {
|
|
project: project.clone(),
|
|
ordered_ids: request
|
|
.ordered_ids
|
|
.iter()
|
|
.map(|id| parse_sprint_id_dto(id))
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?;
|
|
sprint_list(
|
|
SprintListRequestDto {
|
|
project_id: project.id.to_string(),
|
|
},
|
|
state,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn sprint_delete(
|
|
request: SprintDeleteRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
state
|
|
.delete_sprint
|
|
.execute(DeleteSprintInput {
|
|
project,
|
|
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_assign_sprint(
|
|
request: TicketSprintAssignRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.assign_ticket_to_sprint
|
|
.execute(AssignTicketToSprintInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_unassign_sprint(
|
|
request: TicketSprintUnassignRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.unassign_ticket_from_sprint
|
|
.execute(UnassignTicketFromSprintInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_update(
|
|
request: TicketUpdateRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.update_issue
|
|
.execute(update_input(project, request, IssueActor::User)?)
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_read_carnet(
|
|
request: TicketReadRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketCarnetDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
state
|
|
.read_issue_carnet
|
|
.execute(ReadIssueCarnetInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
})
|
|
.await
|
|
.map(|out| TicketCarnetDto::from(out.carnet))
|
|
.map_err(ErrorDto::from)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_update_carnet(
|
|
request: TicketUpdateCarnetRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue_ref = parse_ref_dto(&request.r#ref)?;
|
|
state
|
|
.update_issue_carnet
|
|
.execute(UpdateIssueCarnetInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
carnet: request.carnet,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?;
|
|
let issue = state
|
|
.read_issue
|
|
.execute(ReadIssueInput { project, issue_ref })
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_link(
|
|
request: TicketLinkCommandRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.link_issues
|
|
.execute(LinkIssuesInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
target: parse_ref_dto(&request.target_ref)?,
|
|
kind: parse_link_kind_dto(&request.kind)?,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_unlink(
|
|
request: TicketUnlinkCommandRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let issue = state
|
|
.unlink_issues
|
|
.execute(UnlinkIssuesInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
target: parse_ref_dto(&request.target_ref)?,
|
|
kind: request
|
|
.kind
|
|
.as_deref()
|
|
.map(parse_link_kind_dto)
|
|
.transpose()?,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn ticket_assign(
|
|
request: TicketAssignRequestDto,
|
|
state: State<'_, AppState>,
|
|
) -> Result<TicketDto, ErrorDto> {
|
|
let project = resolve_project(&state, &request.project_id).await?;
|
|
let agent_id = parse_agent_id_dto(&request.agent_id)?;
|
|
let issue = state
|
|
.assign_issue_agent
|
|
.execute(AssignIssueAgentInput {
|
|
project,
|
|
issue_ref: parse_ref_dto(&request.r#ref)?,
|
|
agent_id,
|
|
assigned: request.assigned,
|
|
expected_version: version_dto(request.expected_version)?,
|
|
actor: IssueActor::User,
|
|
})
|
|
.await
|
|
.map_err(ErrorDto::from)?
|
|
.issue;
|
|
Ok(TicketDto::from_issue(issue, None))
|
|
}
|
|
|
|
async fn resolve_project(state: &AppState, project_id: &str) -> Result<Project, ErrorDto> {
|
|
let id = parse_project_id(project_id)?;
|
|
state
|
|
.open_project
|
|
.execute(OpenProjectInput { project_id: id })
|
|
.await
|
|
.map(|out| out.project)
|
|
.map_err(ErrorDto::from)
|
|
}
|
|
|
|
async fn read_carnet_body(
|
|
read_carnet: &application::ReadIssueCarnet,
|
|
project: &Project,
|
|
issue_ref: IssueRef,
|
|
) -> Result<String, TicketToolError> {
|
|
read_carnet
|
|
.execute(ReadIssueCarnetInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
})
|
|
.await
|
|
.map(|out| out.carnet.carnet.as_str().to_owned())
|
|
.map_err(ticket_error)
|
|
}
|
|
|
|
async fn read_ticket_carnet_body_best_effort(
|
|
read_carnet: &application::ReadIssueCarnet,
|
|
project: &Project,
|
|
issue_ref: IssueRef,
|
|
fallback_carnet: String,
|
|
) -> Result<String, AppError> {
|
|
match read_carnet
|
|
.execute(ReadIssueCarnetInput {
|
|
project: project.clone(),
|
|
issue_ref,
|
|
})
|
|
.await
|
|
{
|
|
Ok(out) => Ok(out.carnet.carnet.as_str().to_owned()),
|
|
Err(AppError::NotFound(_)) => Ok(fallback_carnet),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn ticket_error(err: AppError) -> TicketToolError {
|
|
let message = err.to_string();
|
|
let code = if message.contains("issue version conflict") {
|
|
"versionConflict"
|
|
} else {
|
|
match err.code() {
|
|
"NOT_FOUND" => "notFound",
|
|
"INVALID" => "invalid",
|
|
"STORE" => "store",
|
|
"FILESYSTEM" => "filesystem",
|
|
_ => "internal",
|
|
}
|
|
};
|
|
TicketToolError::new(code, message)
|
|
}
|
|
|
|
fn dto_tool_error(err: ErrorDto) -> TicketToolError {
|
|
let code = match err.code.as_str() {
|
|
"NOT_FOUND" => "notFound",
|
|
"INVALID" => "invalid",
|
|
"STORE" => "store",
|
|
"FILESYSTEM" => "filesystem",
|
|
_ => "internal",
|
|
};
|
|
TicketToolError::new(code, err.message)
|
|
}
|
|
|
|
fn mcp_create_request(
|
|
project: &Project,
|
|
arguments: Value,
|
|
) -> Result<TicketCreateRequestDto, TicketToolError> {
|
|
let mut req: TicketCreateRequestDto = serde_json::from_value(arguments)
|
|
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
|
|
req.project_id = project.id.to_string();
|
|
Ok(req)
|
|
}
|
|
|
|
fn mcp_update_request(
|
|
project: &Project,
|
|
arguments: Value,
|
|
) -> Result<TicketUpdateRequestDto, TicketToolError> {
|
|
let mut req: TicketUpdateRequestDto = serde_json::from_value(arguments)
|
|
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
|
|
req.project_id = project.id.to_string();
|
|
Ok(req)
|
|
}
|
|
|
|
fn mcp_list_request(
|
|
project: &Project,
|
|
arguments: Value,
|
|
) -> Result<TicketListPageInput, TicketToolError> {
|
|
let mut req: TicketListRequestDto = serde_json::from_value(arguments)
|
|
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
|
|
req.project_id = project.id.to_string();
|
|
TicketListPageInput::from_request(req).map_err(|e| TicketToolError::new("invalid", e.message))
|
|
}
|
|
|
|
fn mcp_bulk_update_status_request(
|
|
project: &Project,
|
|
arguments: Value,
|
|
) -> Result<TicketBulkUpdateStatusRequestDto, TicketToolError> {
|
|
let mut req: TicketBulkUpdateStatusRequestDto = serde_json::from_value(arguments)
|
|
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
|
|
req.project_id = project.id.to_string();
|
|
Ok(req)
|
|
}
|
|
|
|
fn mcp_bulk_update_priority_request(
|
|
project: &Project,
|
|
arguments: Value,
|
|
) -> Result<TicketBulkUpdatePriorityRequestDto, TicketToolError> {
|
|
let mut req: TicketBulkUpdatePriorityRequestDto = 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))
|
|
}
|
|
|
|
fn parse_json_version(arguments: &Value) -> Result<domain::IssueVersion, TicketToolError> {
|
|
let version = arguments
|
|
.get("expectedVersion")
|
|
.and_then(Value::as_u64)
|
|
.ok_or_else(|| TicketToolError::new("invalid", "missing expectedVersion"))?;
|
|
version_dto(version).map_err(dto_tool_error)
|
|
}
|
|
|
|
fn required_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, TicketToolError> {
|
|
arguments
|
|
.get(key)
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| TicketToolError::new("invalid", format!("missing {key}")))
|
|
}
|