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:
@ -12,15 +12,15 @@ use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
AgentResumer, AgentWakeService, AppError, AssignIssueAgent, AssignSkillToAgent,
|
||||
AssignTicketToSprint, AttachLiveAgent, AuthenticateSession, BackgroundCommandArchive,
|
||||
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,
|
||||
AddIssueAttachment, AgentResumer, AgentWakeService, AppError, AssignIssueAgent,
|
||||
AssignSkillToAgent, AssignTicketToSprint, AttachLiveAgent, AuthenticateSession,
|
||||
BackgroundCommandArchive, 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,
|
||||
@ -31,12 +31,13 @@ use application::{
|
||||
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,
|
||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout,
|
||||
MarkIssueAttachmentSummarized, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
|
||||
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
|
||||
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
|
||||
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
|
||||
ReadIssueAttachment, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex,
|
||||
ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
|
||||
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
|
||||
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
||||
@ -997,6 +998,12 @@ pub struct BackendCore {
|
||||
pub read_issue_carnet: Arc<ReadIssueCarnet>,
|
||||
/// Update a ticket carnet.
|
||||
pub update_issue_carnet: Arc<UpdateIssueCarnet>,
|
||||
/// Attach a local file to a ticket.
|
||||
pub add_issue_attachment: Arc<AddIssueAttachment>,
|
||||
/// Read one ticket attachment.
|
||||
pub read_issue_attachment: Arc<ReadIssueAttachment>,
|
||||
/// Mark a ticket attachment as summarized in the carnet.
|
||||
pub mark_issue_attachment_summarized: Arc<MarkIssueAttachmentSummarized>,
|
||||
/// Link two public tickets.
|
||||
pub link_issues: Arc<LinkIssues>,
|
||||
/// Unlink public tickets.
|
||||
@ -1696,6 +1703,19 @@ impl BackendCore {
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let add_issue_attachment = Arc::new(AddIssueAttachment::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let read_issue_attachment =
|
||||
Arc::new(ReadIssueAttachment::new(Arc::clone(&issue_store_port)));
|
||||
let mark_issue_attachment_summarized = Arc::new(MarkIssueAttachmentSummarized::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let link_issues = Arc::new(LinkIssues::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
@ -2843,6 +2863,9 @@ impl BackendCore {
|
||||
bulk_delete_issues,
|
||||
read_issue_carnet,
|
||||
update_issue_carnet,
|
||||
add_issue_attachment,
|
||||
read_issue_attachment,
|
||||
mark_issue_attachment_summarized,
|
||||
link_issues,
|
||||
unlink_issues,
|
||||
assign_issue_agent,
|
||||
|
||||
@ -349,6 +349,8 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
|
||||
| "idea_ticket_update_status"
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_attachment_add"
|
||||
| "idea_ticket_attachment_mark_summarized"
|
||||
| "idea_ticket_link"
|
||||
| "idea_ticket_unlink"
|
||||
)
|
||||
|
||||
@ -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