This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OutlookAddin/src/OutlookAddin.Core/Services/IAttachmentSaveService.cs
T
PointStar 5242abb585 feat(attachments): rewire to Marcus-Law NetworkStorageIntegration
After reading the NetworkStorageIntegration repo I realized the previous
"save attachments" flow was wrong on every level — Marcus-Law doesn't
use EspoCRM's stock Document/DocumentFolder model. Folders are real
filesystem paths under the network share, named after the Case (format
"<number>-<primary contact name>") with four standard subfolders:
מסמכים, אסמכתאות, התכתבויות, כללי.

Rewire the save pipeline against the integration's real endpoints
(documented in routes.json on the server repo):

Core — three new IEspoCrmClient methods:
- EnsureCaseSubfoldersAsync(caseId)
    POST /NetworkStorage/action/createSubfolders
    Idempotent — creates case folder + defaults if missing.
- ListNetworkStorageFolderAsync(path)
    GET /NetworkStorage/folderContents?path=…
    Returns folders + files; we filter to type=folder for the dropdown.
- UploadAttachmentToNetworkStorageAsync(attachmentId, targetPath)
    POST /NetworkStorage/action/upload
    Moves the staged Attachment binary onto the share at <path>/<name>.

CaseEntity gets a new `networkStorageFolderPath` field (set by the
integration's hook on first Document save; empty until then).

NetworkStorageItem DTO captures the folder-listing rows.

AttachmentSaveService rewritten — no more Document creation, no more
DocumentFolder picker. The new contract is:
    SaveAsync(attachments, targetFolderPath)
Where targetFolderPath = "<case folder>[/subfolder]". For each
attachment: upload binary via standard /Attachment, then move via
NetworkStorage upload.

UI — SaveAttachmentsViewModel:
- Subfolder dropdown switched from DocumentFolder objects to strings.
- Static defaults loaded up-front:
    "(שורש התיק)" + 4 server-side defaults.
- When the user picks a case:
    1) hydrate Case (number + networkStorageFolderPath) via GetCase
    2) call EnsureCaseSubfoldersAsync to materialise standard folders
    3) listFolder at the case path to merge any custom subfolders into
       the dropdown.
- ResolveCaseFolderPath: use Case.networkStorageFolderPath if set,
  otherwise compute "<number>-<contact-name>" with a client-side
  SanitizeFolderSegment that mirrors LocalFilesystemClient::sanitizeFileName
  on the server (geresh/gershayim/quotes removed, illegal chars → hyphen).

AddInHost.LaunchSaveAttachmentsAsync:
- Composes <caseFolder>[/subfolder] from the VM choice.
- Pre-flight ensureCaseSubfolders as a safety net (in case the user
  submitted before the dialog's lazy refresh completed).
- Summary MessageBox now shows the resolved storage path instead of a
  bare entity name.

Tests: 39 passing. The build is clean across Core / UI / Tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:42:22 +03:00

80 lines
2.3 KiB
C#

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
namespace MarcusLaw.OutlookAddin.Core.Services
{
public interface IAttachmentSaveService
{
/// <summary>
/// Uploads each attachment as an EspoCRM Attachment, then asks
/// Marcus-Law's NetworkStorage integration to place the file under
/// <paramref name="targetFolderPath"/> on the shared storage. No
/// Document entity is created — these are raw files dropped into
/// the case's filesystem folder.
/// </summary>
/// <param name="attachments">files to save</param>
/// <param name="targetFolderPath">
/// Full relative folder path on storage, e.g.
/// <c>"30-מרדכי שמביקו/אסמכתאות"</c>. The case folder + optional
/// subfolder. Filename is appended internally.
/// </param>
Task<AttachmentSaveSummary> SaveAsync(
IReadOnlyList<EspoAttachment> attachments,
string targetFolderPath,
CancellationToken cancellationToken = default);
}
public sealed class AttachmentSaveSummary
{
public List<AttachmentSaveItemResult> Items { get; set; } = new List<AttachmentSaveItemResult>();
public bool AuthRequired { get; set; }
public int SavedCount
{
get
{
var n = 0;
foreach (var it in Items)
{
if (it.Outcome == AttachmentSaveOutcome.Saved) n++;
}
return n;
}
}
public int FailedCount
{
get
{
var n = 0;
foreach (var it in Items)
{
if (it.Outcome == AttachmentSaveOutcome.Failed) n++;
}
return n;
}
}
}
public sealed class AttachmentSaveItemResult
{
public string FileName { get; set; } = string.Empty;
public AttachmentSaveOutcome Outcome { get; set; }
public string? AttachmentId { get; set; }
public string? ErrorMessage { get; set; }
}
public enum AttachmentSaveOutcome
{
Saved,
Failed,
AuthRequired
}
}