feat(tickets): ajoute les pièces jointes sur tickets (#108)
Stockage flat côté ticket + métadonnées d'attachments, lecture exposée côté backend/MCP, et UI minimale de liste/ajout dans TicketDetail. Traverse le domaine (Issue, ports), l'application (usecases + assistant de ticket), les adaptateurs infra/MCP (issues store, orchestrateur), les DTO backend/web- server/app-tauri, et le frontend (domain/ports/adapters/hooks/UI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -10,9 +10,10 @@ use application::{
|
||||
BulkUpdateIssueStatusInput, CreateIssueInput, UpdateIssueInput,
|
||||
};
|
||||
use domain::{
|
||||
AgentId, AgentIssueRole, Issue, IssueActor, IssueCarnet, IssueIndexEntry, IssueLink,
|
||||
IssueLinkKind, IssueListFilter, IssuePriority, IssueRef, IssueStatus, IssueVersion, ProfileId,
|
||||
Project, SprintId, SprintStatus, SprintVersion,
|
||||
AgentId, AgentIssueRole, Issue, IssueActor, IssueAttachment, IssueAttachmentContent,
|
||||
IssueAttachmentId, IssueCarnet, IssueIndexEntry, IssueLink, IssueLinkKind, IssueListFilter,
|
||||
IssuePriority, IssueRef, IssueStatus, IssueVersion, ProfileId, Project, SprintId, SprintStatus,
|
||||
SprintVersion,
|
||||
};
|
||||
use infrastructure::TicketToolError;
|
||||
|
||||
@ -36,6 +37,7 @@ pub struct TicketDto {
|
||||
pub carnet: Option<String>,
|
||||
pub links: Vec<TicketLinkDto>,
|
||||
pub assigned_agent_ids: Vec<String>,
|
||||
pub attachments: Vec<TicketAttachmentDto>,
|
||||
pub created_by: TicketActorDto,
|
||||
pub updated_by: TicketActorDto,
|
||||
pub created_at: u64,
|
||||
@ -69,6 +71,31 @@ pub struct TicketListDto {
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
/// Public attachment metadata DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketAttachmentDto {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub mime: String,
|
||||
pub size_bytes: u64,
|
||||
pub added_by: TicketActorDto,
|
||||
pub added_at: u64,
|
||||
pub summarized_in_carnet: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summarized_by: Option<TicketActorDto>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summarized_at: Option<u64>,
|
||||
}
|
||||
|
||||
/// Raw attachment content DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketAttachmentContentDto {
|
||||
pub attachment: TicketAttachmentDto,
|
||||
pub content_base64: String,
|
||||
}
|
||||
|
||||
/// Sprint context embedded in ticket read/list responses.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -186,6 +213,39 @@ pub struct TicketDeleteRequestDto {
|
||||
pub r#ref: String,
|
||||
}
|
||||
|
||||
/// Add attachment request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketAttachmentAddRequestDto {
|
||||
#[serde(default)]
|
||||
pub project_id: String,
|
||||
pub r#ref: String,
|
||||
pub path: String,
|
||||
pub mime: Option<String>,
|
||||
pub expected_version: u64,
|
||||
}
|
||||
|
||||
/// Read attachment request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketAttachmentReadRequestDto {
|
||||
#[serde(default)]
|
||||
pub project_id: String,
|
||||
pub r#ref: String,
|
||||
pub attachment_id: String,
|
||||
}
|
||||
|
||||
/// Mark attachment summarized request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketAttachmentMarkSummarizedRequestDto {
|
||||
#[serde(default)]
|
||||
pub project_id: String,
|
||||
pub r#ref: String,
|
||||
pub attachment_id: String,
|
||||
pub expected_version: u64,
|
||||
}
|
||||
|
||||
/// List request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -437,6 +497,11 @@ impl TicketDto {
|
||||
.filter(|r| r.role == AgentIssueRole::Assigned)
|
||||
.map(|r| r.agent_id.to_string())
|
||||
.collect(),
|
||||
attachments: issue
|
||||
.attachments
|
||||
.into_iter()
|
||||
.map(TicketAttachmentDto::from)
|
||||
.collect(),
|
||||
created_by: TicketActorDto::from(issue.created_by),
|
||||
updated_by: TicketActorDto::from(issue.updated_by),
|
||||
created_at: issue.created_at,
|
||||
@ -547,6 +612,31 @@ impl From<IssueActor> for TicketActorDto {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IssueAttachment> for TicketAttachmentDto {
|
||||
fn from(attachment: IssueAttachment) -> Self {
|
||||
Self {
|
||||
id: attachment.id.as_str().to_owned(),
|
||||
filename: attachment.filename,
|
||||
mime: attachment.mime,
|
||||
size_bytes: attachment.size_bytes,
|
||||
added_by: TicketActorDto::from(attachment.added_by),
|
||||
added_at: attachment.added_at,
|
||||
summarized_in_carnet: attachment.summarized_in_carnet,
|
||||
summarized_by: attachment.summarized_by.map(TicketActorDto::from),
|
||||
summarized_at: attachment.summarized_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IssueAttachmentContent> for TicketAttachmentContentDto {
|
||||
fn from(content: IssueAttachmentContent) -> Self {
|
||||
Self {
|
||||
attachment: TicketAttachmentDto::from(content.attachment),
|
||||
content_base64: URL_SAFE_NO_PAD.encode(content.bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TicketCreatorFilterDto> for IssueActor {
|
||||
type Error = ErrorDto;
|
||||
|
||||
@ -560,6 +650,13 @@ impl TryFrom<TicketCreatorFilterDto> for IssueActor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_attachment_id_dto(raw: &str) -> Result<IssueAttachmentId, ErrorDto> {
|
||||
IssueAttachmentId::new(raw.to_owned()).map_err(|err| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<IssueCarnet> for TicketCarnetDto {
|
||||
fn from(carnet: IssueCarnet) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user