b1f8e3e50b
New workflow alongside "תייק ל-Klear": save just the attachments of a
mail to a Case in EspoCRM (no Email entity created), with an optional
DocumentFolder.
Core:
- AttachmentEntity, DocumentEntity, DocumentFolder DTOs
- IEspoCrmClient gets three new endpoints:
UploadAttachmentAsync (POST /Attachment, data-URI base64, role=Attachment)
CreateDocumentAsync (POST /Document, fileId + parent + folderId)
ListDocumentFoldersAsync (GET /DocumentFolder, returns envelope)
- AttachmentSaveService orchestrates per-attachment upload + Document
create, short-circuits on EspoCrmAuthorizationException, aggregates
per-file Saved/Failed/AuthRequired outcomes into AttachmentSaveSummary
UI:
- SaveAttachmentsDialog (RTL, card layout matching FileToCaseDialog):
Case search + folder dropdown + per-attachment checkbox list
- SaveAttachmentsViewModel: debounced GlobalSearch filtered to Case,
optional folder selector (with a "(no folder)" sentinel row),
Attachments collection of AttachmentRowViewModel rows
- AttachmentRowViewModel: IsSelected checkbox + display name + size
computed from base64 length
Host:
- AddInHost.LaunchSaveAttachmentsAsync(MailItem) — extracts on the STA
via MailItemExtractor, fetches folders, shows the dialog (anchored to
Outlook via AttachOwnerToOutlook), runs AttachmentSaveService on OK,
reports a Hebrew MessageBox summary
- AttachmentSaveService field on AddInHost + dispose hook
Ribbon:
- New "שמור קבצים" button in the Marcus-Law group, between Klear and
פרטי תיק. Enabled only when exactly one MailItem is selected and it
has at least one attachment
- klear-attach.png paperclip icon (32x32, blue rounded square),
embedded as a resource and resolved by the existing OnGetImage
callback (new case in the switch)
Tests: 39 still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.7 KiB
C#
98 lines
3.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using MarcusLaw.OutlookAddin.Core.Models;
|
|
using Serilog;
|
|
|
|
namespace MarcusLaw.OutlookAddin.Core.Services
|
|
{
|
|
public sealed class AttachmentSaveService : IAttachmentSaveService
|
|
{
|
|
private readonly IEspoCrmClient _client;
|
|
private readonly ILogger _logger;
|
|
|
|
public AttachmentSaveService(IEspoCrmClient client, ILogger logger)
|
|
{
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async Task<AttachmentSaveSummary> SaveAsync(
|
|
IReadOnlyList<EspoAttachment> attachments,
|
|
FilingTarget target,
|
|
string? folderId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (attachments == null) throw new ArgumentNullException(nameof(attachments));
|
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
|
|
|
var summary = new AttachmentSaveSummary();
|
|
if (attachments.Count == 0) return summary;
|
|
|
|
foreach (var att in attachments)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (summary.AuthRequired)
|
|
{
|
|
summary.Items.Add(new AttachmentSaveItemResult
|
|
{
|
|
FileName = att.Name,
|
|
Outcome = AttachmentSaveOutcome.AuthRequired,
|
|
ErrorMessage = "Skipped — earlier upload rejected by EspoCRM"
|
|
});
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var uploaded = await _client.UploadAttachmentAsync(
|
|
att.Name, att.ContentType, att.ContentBase64, cancellationToken).ConfigureAwait(false);
|
|
|
|
var doc = await _client.CreateDocumentAsync(
|
|
att.Name, uploaded.Id, target.ParentType, target.ParentId, folderId, cancellationToken).ConfigureAwait(false);
|
|
|
|
_logger.Information(
|
|
"Saved attachment {Name} → Document/{DocId} (parent={ParentType}/{ParentId}, folder={FolderId})",
|
|
att.Name, doc.Id, target.ParentType, target.ParentId, folderId ?? "(none)");
|
|
|
|
summary.Items.Add(new AttachmentSaveItemResult
|
|
{
|
|
FileName = att.Name,
|
|
Outcome = AttachmentSaveOutcome.Saved,
|
|
DocumentId = doc.Id
|
|
});
|
|
}
|
|
catch (EspoCrmAuthorizationException ex)
|
|
{
|
|
_logger.Warning(ex, "Attachment save aborted: EspoCRM rejected credentials");
|
|
summary.AuthRequired = true;
|
|
summary.Items.Add(new AttachmentSaveItemResult
|
|
{
|
|
FileName = att.Name,
|
|
Outcome = AttachmentSaveOutcome.AuthRequired,
|
|
ErrorMessage = ex.Message
|
|
});
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warning(ex, "Attachment save failed for {Name}", att.Name);
|
|
summary.Items.Add(new AttachmentSaveItemResult
|
|
{
|
|
FileName = att.Name,
|
|
Outcome = AttachmentSaveOutcome.Failed,
|
|
ErrorMessage = ex.Message
|
|
});
|
|
}
|
|
}
|
|
|
|
return summary;
|
|
}
|
|
}
|
|
}
|