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:
@ -3,17 +3,22 @@
|
||||
//! The domain/code term is `Issue` throughout this layer.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{AgentContextStore, Clock, EventBus, IdGenerator};
|
||||
use domain::{
|
||||
AgentId, AgentIssueRef, AgentIssueRole, DomainEvent, Issue, IssueActor, IssueCarnet, IssueId,
|
||||
IssueIndexEntry, IssueLink, IssueLinkKind, IssueListFilter, IssueNumberAllocator,
|
||||
IssuePriority, IssueRef, IssueStatus, IssueStore, IssueVersion, MarkdownDoc, Project,
|
||||
AgentId, AgentIssueRef, AgentIssueRole, DomainEvent, Issue, IssueActor, IssueAttachmentContent,
|
||||
IssueAttachmentId, IssueCarnet, IssueId, IssueIndexEntry, IssueLink, IssueLinkKind,
|
||||
IssueListFilter, IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore,
|
||||
IssueVersion, LocalPath, MarkdownDoc, Project,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// Maximum raw attachment size for the minimal ticket attachment lot.
|
||||
pub const TICKET_ATTACHMENT_MAX_BYTES: u64 = 25 * 1024 * 1024;
|
||||
|
||||
/// Input for [`CreateIssue::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateIssueInput {
|
||||
@ -720,6 +725,199 @@ impl UpdateIssueCarnet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`AddIssueAttachment::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddIssueAttachmentInput {
|
||||
/// Project owning the issue.
|
||||
pub project: Project,
|
||||
/// Issue reference.
|
||||
pub issue_ref: IssueRef,
|
||||
/// Local source file path.
|
||||
pub path: String,
|
||||
/// Optional MIME type supplied by the driving adapter.
|
||||
pub mime: Option<String>,
|
||||
/// Expected optimistic version.
|
||||
pub expected_version: IssueVersion,
|
||||
/// Actor adding the attachment.
|
||||
pub actor: IssueActor,
|
||||
}
|
||||
|
||||
/// Output of [`AddIssueAttachment::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AddIssueAttachmentOutput {
|
||||
/// Updated issue.
|
||||
pub issue: Issue,
|
||||
}
|
||||
|
||||
/// Adds a local file as a ticket attachment.
|
||||
pub struct AddIssueAttachment {
|
||||
issues: Arc<dyn IssueStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl AddIssueAttachment {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
issues: Arc<dyn IssueStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
issues,
|
||||
ids,
|
||||
clock,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes attachment add.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: AddIssueAttachmentInput,
|
||||
) -> Result<AddIssueAttachmentOutput, AppError> {
|
||||
let filename = filename_from_path(&input.path)?;
|
||||
validate_attachment_filename(&filename)?;
|
||||
let mime = sanitize_mime(input.mime.as_deref(), &filename)?;
|
||||
let attachment_id = IssueAttachmentId::new(self.ids.new_uuid().to_string())
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||
let issue = self
|
||||
.issues
|
||||
.add_attachment_from_path(
|
||||
&input.project.root,
|
||||
input.issue_ref,
|
||||
&LocalPath::new(input.path),
|
||||
attachment_id,
|
||||
filename,
|
||||
mime,
|
||||
input.actor,
|
||||
now(&self.clock),
|
||||
input.expected_version,
|
||||
)
|
||||
.await?;
|
||||
self.events.publish(DomainEvent::IssueUpdated {
|
||||
issue_ref: input.issue_ref,
|
||||
version: issue.version,
|
||||
});
|
||||
Ok(AddIssueAttachmentOutput { issue })
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`ReadIssueAttachment::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadIssueAttachmentInput {
|
||||
/// Project owning the issue.
|
||||
pub project: Project,
|
||||
/// Issue reference.
|
||||
pub issue_ref: IssueRef,
|
||||
/// Attachment id.
|
||||
pub attachment_id: IssueAttachmentId,
|
||||
}
|
||||
|
||||
/// Output of [`ReadIssueAttachment::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadIssueAttachmentOutput {
|
||||
/// Attachment content.
|
||||
pub content: IssueAttachmentContent,
|
||||
}
|
||||
|
||||
/// Reads a ticket attachment.
|
||||
pub struct ReadIssueAttachment {
|
||||
issues: Arc<dyn IssueStore>,
|
||||
}
|
||||
|
||||
impl ReadIssueAttachment {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(issues: Arc<dyn IssueStore>) -> Self {
|
||||
Self { issues }
|
||||
}
|
||||
|
||||
/// Executes attachment read.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ReadIssueAttachmentInput,
|
||||
) -> Result<ReadIssueAttachmentOutput, AppError> {
|
||||
Ok(ReadIssueAttachmentOutput {
|
||||
content: self
|
||||
.issues
|
||||
.read_attachment(&input.project.root, input.issue_ref, &input.attachment_id)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`MarkIssueAttachmentSummarized::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MarkIssueAttachmentSummarizedInput {
|
||||
/// Project owning the issue.
|
||||
pub project: Project,
|
||||
/// Issue reference.
|
||||
pub issue_ref: IssueRef,
|
||||
/// Attachment id.
|
||||
pub attachment_id: IssueAttachmentId,
|
||||
/// Expected optimistic version.
|
||||
pub expected_version: IssueVersion,
|
||||
/// Actor marking summary state.
|
||||
pub actor: IssueActor,
|
||||
}
|
||||
|
||||
/// Output of [`MarkIssueAttachmentSummarized::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MarkIssueAttachmentSummarizedOutput {
|
||||
/// Updated issue.
|
||||
pub issue: Issue,
|
||||
}
|
||||
|
||||
/// Marks an attachment as summarized in the ticket carnet.
|
||||
pub struct MarkIssueAttachmentSummarized {
|
||||
issues: Arc<dyn IssueStore>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl MarkIssueAttachmentSummarized {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
issues: Arc<dyn IssueStore>,
|
||||
clock: Arc<dyn Clock>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
issues,
|
||||
clock,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes summary mark.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: MarkIssueAttachmentSummarizedInput,
|
||||
) -> Result<MarkIssueAttachmentSummarizedOutput, AppError> {
|
||||
let issue = self
|
||||
.issues
|
||||
.mark_attachment_summarized(
|
||||
&input.project.root,
|
||||
input.issue_ref,
|
||||
&input.attachment_id,
|
||||
input.actor,
|
||||
now(&self.clock),
|
||||
input.expected_version,
|
||||
)
|
||||
.await?;
|
||||
self.events.publish(DomainEvent::IssueUpdated {
|
||||
issue_ref: input.issue_ref,
|
||||
version: issue.version,
|
||||
});
|
||||
Ok(MarkIssueAttachmentSummarizedOutput { issue })
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`LinkIssues::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LinkIssuesInput {
|
||||
@ -1030,6 +1228,75 @@ fn replace_assigned_agents(issue: &mut Issue, agent_ids: Vec<AgentId>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn filename_from_path(path: &str) -> Result<String, AppError> {
|
||||
Path::new(path)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| AppError::Invalid("attachment path has no filename".to_owned()))
|
||||
}
|
||||
|
||||
fn validate_attachment_filename(filename: &str) -> Result<(), AppError> {
|
||||
let lowered = filename.to_ascii_lowercase();
|
||||
let blocked = [
|
||||
"exe", "bat", "cmd", "com", "scr", "msi", "dll", "so", "dylib", "sh", "ps1", "jar", "app",
|
||||
"deb", "rpm",
|
||||
];
|
||||
if lowered.trim().is_empty()
|
||||
|| lowered.contains('/')
|
||||
|| lowered.contains('\\')
|
||||
|| lowered == "."
|
||||
|| lowered == ".."
|
||||
{
|
||||
return Err(AppError::Invalid("invalid attachment filename".to_owned()));
|
||||
}
|
||||
if lowered
|
||||
.rsplit_once('.')
|
||||
.is_some_and(|(_, ext)| blocked.contains(&ext))
|
||||
{
|
||||
return Err(AppError::Invalid(
|
||||
"executable attachments are not allowed".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sanitize_mime(raw: Option<&str>, filename: &str) -> Result<String, AppError> {
|
||||
let mime = raw
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| infer_mime(filename).to_owned());
|
||||
if mime.eq_ignore_ascii_case("application/x-msdownload")
|
||||
|| mime.eq_ignore_ascii_case("application/x-sh")
|
||||
|| mime.eq_ignore_ascii_case("application/x-executable")
|
||||
{
|
||||
return Err(AppError::Invalid(
|
||||
"executable attachments are not allowed".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(mime)
|
||||
}
|
||||
|
||||
fn infer_mime(filename: &str) -> &'static str {
|
||||
match filename
|
||||
.rsplit_once('.')
|
||||
.map(|(_, ext)| ext.to_ascii_lowercase())
|
||||
.as_deref()
|
||||
{
|
||||
Some("txt" | "md" | "log") => "text/plain",
|
||||
Some("json") => "application/json",
|
||||
Some("xml") => "application/xml",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("png") => "image/png",
|
||||
Some("gif") => "image/gif",
|
||||
Some("webp") => "image/webp",
|
||||
Some("pdf") => "application/pdf",
|
||||
Some("csv") => "text/csv",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_update_events(
|
||||
events: &Arc<dyn EventBus>,
|
||||
issue: &Issue,
|
||||
|
||||
@ -96,14 +96,18 @@ pub use git::{
|
||||
};
|
||||
pub use health::{HealthInput, HealthReport, HealthUseCase};
|
||||
pub use issues::{
|
||||
AssignIssueAgent, AssignIssueAgentInput, AssignIssueAgentOutput, BatchIssueResult,
|
||||
BulkDeleteIssues, BulkDeleteIssuesInput, BulkIssueMutationOutput, BulkUpdateIssuePriority,
|
||||
AddIssueAttachment, AddIssueAttachmentInput, AddIssueAttachmentOutput, AssignIssueAgent,
|
||||
AssignIssueAgentInput, AssignIssueAgentOutput, BatchIssueResult, BulkDeleteIssues,
|
||||
BulkDeleteIssuesInput, BulkIssueMutationOutput, BulkUpdateIssuePriority,
|
||||
BulkUpdateIssuePriorityInput, BulkUpdateIssueStatus, BulkUpdateIssueStatusInput, CreateIssue,
|
||||
CreateIssueInput, CreateIssueOutput, DeleteIssue, DeleteIssueInput, DeleteIssueOutput,
|
||||
LinkIssues, LinkIssuesInput, LinkIssuesOutput, ListIssues, ListIssuesInput, ListIssuesOutput,
|
||||
ReadIssue, ReadIssueCarnet, ReadIssueCarnetInput, ReadIssueCarnetOutput, ReadIssueInput,
|
||||
ReadIssueOutput, UnlinkIssues, UnlinkIssuesInput, UpdateIssue, UpdateIssueCarnet,
|
||||
UpdateIssueCarnetInput, UpdateIssueCarnetOutput, UpdateIssueInput, UpdateIssueOutput,
|
||||
MarkIssueAttachmentSummarized, MarkIssueAttachmentSummarizedInput,
|
||||
MarkIssueAttachmentSummarizedOutput, ReadIssue, ReadIssueAttachment, ReadIssueAttachmentInput,
|
||||
ReadIssueAttachmentOutput, ReadIssueCarnet, ReadIssueCarnetInput, ReadIssueCarnetOutput,
|
||||
ReadIssueInput, ReadIssueOutput, UnlinkIssues, UnlinkIssuesInput, UpdateIssue,
|
||||
UpdateIssueCarnet, UpdateIssueCarnetInput, UpdateIssueCarnetOutput, UpdateIssueInput,
|
||||
UpdateIssueOutput, TICKET_ATTACHMENT_MAX_BYTES,
|
||||
};
|
||||
pub use layout::{
|
||||
CreateLayout, CreateLayoutInput, CreateLayoutOutput, DeleteLayout, DeleteLayoutInput,
|
||||
|
||||
@ -139,6 +139,8 @@ impl OpenTicketAssistant {
|
||||
"idea_ticket_update_priority".to_owned(),
|
||||
"idea_ticket_read_carnet".to_owned(),
|
||||
"idea_ticket_update_carnet".to_owned(),
|
||||
"idea_ticket_attachment_read".to_owned(),
|
||||
"idea_ticket_attachment_mark_summarized".to_owned(),
|
||||
"idea_ticket_link".to_owned(),
|
||||
"idea_ticket_unlink".to_owned(),
|
||||
],
|
||||
|
||||
@ -8,9 +8,10 @@ use domain::ports::{
|
||||
IssueStoreError, StoreError,
|
||||
};
|
||||
use domain::{
|
||||
AgentId, AgentManifest, DomainEvent, Issue, IssueActor, IssueCarnet, IssueIndexEntry,
|
||||
IssueListFilter, IssueNumber, IssuePriority, IssueRef, IssueStatus, IssueVersion,
|
||||
ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectId, ProjectPath, RemoteRef,
|
||||
AgentId, AgentManifest, DomainEvent, Issue, IssueActor, IssueAttachmentContent,
|
||||
IssueAttachmentId, IssueCarnet, IssueIndexEntry, IssueListFilter, IssueNumber, IssuePriority,
|
||||
IssueRef, IssueStatus, IssueVersion, LocalPath, ManifestEntry, MarkdownDoc, ProfileId, Project,
|
||||
ProjectId, ProjectPath, RemoteRef,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -139,6 +140,42 @@ impl IssueStore for FakeIssues {
|
||||
self.update(_root, &updated, expected_version).await?;
|
||||
self.read_carnet(_root, issue_ref).await
|
||||
}
|
||||
|
||||
async fn add_attachment_from_path(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_source: &LocalPath,
|
||||
_attachment_id: IssueAttachmentId,
|
||||
_filename: String,
|
||||
_mime: String,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
|
||||
async fn read_attachment(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
) -> Result<IssueAttachmentContent, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
|
||||
async fn mark_attachment_summarized(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
}
|
||||
|
||||
struct SeqAllocator(Mutex<u64>);
|
||||
|
||||
@ -4,10 +4,11 @@ use std::sync::{Arc, Mutex};
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{Clock, EventBus, EventStream, IdGenerator, IssueStore, IssueStoreError};
|
||||
use domain::{
|
||||
DomainEvent, Issue, IssueActor, IssueCarnet, IssueId, IssueIndexEntry, IssueListFilter,
|
||||
IssueNumber, IssuePriority, IssueRef, IssueStatus, IssueVersion, MarkdownDoc, Project,
|
||||
ProjectId, ProjectPath, RemoteRef, Sprint, SprintId, SprintIndexEntry, SprintOrder,
|
||||
SprintStatus, SprintStore, SprintStoreError, SprintVersion,
|
||||
DomainEvent, Issue, IssueActor, IssueAttachmentContent, IssueAttachmentId, IssueCarnet,
|
||||
IssueId, IssueIndexEntry, IssueListFilter, IssueNumber, IssuePriority, IssueRef, IssueStatus,
|
||||
IssueVersion, LocalPath, MarkdownDoc, Project, ProjectId, ProjectPath, RemoteRef, Sprint,
|
||||
SprintId, SprintIndexEntry, SprintOrder, SprintStatus, SprintStore, SprintStoreError,
|
||||
SprintVersion,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -198,6 +199,42 @@ impl IssueStore for FakeIssues {
|
||||
) -> Result<IssueCarnet, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
|
||||
async fn add_attachment_from_path(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_source: &LocalPath,
|
||||
_attachment_id: IssueAttachmentId,
|
||||
_filename: String,
|
||||
_mime: String,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
|
||||
async fn read_attachment(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
) -> Result<IssueAttachmentContent, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
|
||||
async fn mark_attachment_summarized(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!("not needed")
|
||||
}
|
||||
}
|
||||
|
||||
struct SeqIds(Mutex<u128>);
|
||||
|
||||
@ -17,11 +17,11 @@ use domain::profile::StructuredAdapter;
|
||||
use domain::{
|
||||
AgentId, AgentPermissionOverride, AgentProfile, AgentToolPolicy, AgentToolPolicyStore,
|
||||
AssistantContextError, AssistantContextProvider, ContextInjection, DomainEvent, EventBus,
|
||||
EventStream, Issue, IssueActor, IssueCarnet, IssueId, IssueListFilter, IssueNumber,
|
||||
IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError, IssueVersion, MarkdownDoc,
|
||||
NetworkPolicy, PermissionSet, PreparedContext, ProfileId, ProfileStore, Project, ProjectId,
|
||||
ProjectPath, ProjectPermissions, ProjectSystemPermissions, RemoteRef, SessionId,
|
||||
SystemPermissionSet,
|
||||
EventStream, Issue, IssueActor, IssueAttachmentContent, IssueAttachmentId, IssueCarnet,
|
||||
IssueId, IssueListFilter, IssueNumber, IssuePriority, IssueRef, IssueStatus, IssueStore,
|
||||
IssueStoreError, IssueVersion, LocalPath, MarkdownDoc, NetworkPolicy, PermissionSet,
|
||||
PreparedContext, ProfileId, ProfileStore, Project, ProjectId, ProjectPath, ProjectPermissions,
|
||||
ProjectSystemPermissions, RemoteRef, SessionId, SystemPermissionSet,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -138,6 +138,42 @@ impl IssueStore for FakeIssues {
|
||||
) -> Result<IssueCarnet, IssueStoreError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn add_attachment_from_path(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_source: &LocalPath,
|
||||
_attachment_id: IssueAttachmentId,
|
||||
_filename: String,
|
||||
_mime: String,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn read_attachment(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
) -> Result<IssueAttachmentContent, IssueStoreError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn mark_attachment_summarized(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
_attachment_id: &IssueAttachmentId,
|
||||
_actor: IssueActor,
|
||||
_now_ms: u64,
|
||||
_expected_version: IssueVersion,
|
||||
) -> Result<Issue, IssueStoreError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeProfiles {
|
||||
|
||||
Reference in New Issue
Block a user