feat(save-attachments): scope folder list to the selected case
The folder dropdown was showing every DocumentFolder in EspoCRM (global
list). The user expected "folders that exist in THIS case" — only the
folders already in use for the chosen case's documents.
Change: when the user picks a case in the SaveAttachments dialog, the
folder list refreshes by walking
GET /Case/{id}/documents?select=id,folderId,folderName&maxSize=200
and deduping the folderId values it finds. EspoCRM denormalizes
folderName onto Document, so no second lookup is needed.
If a case has no documents yet, the dropdown shows just the sentinel
"(בלי תיקייה)" — same default behavior as before, but no more
unrelated folders from other cases.
Files:
- Core: DocumentEntity gets FolderName; IEspoCrmClient and EspoCrmClient
get ListFoldersUsedInCaseAsync(caseId)
- UI: SaveAttachmentsViewModel re-fetches on SelectedCase change
(LoadFoldersForCaseAsync); AddInHost no longer calls the old
LoadFoldersAsync.
Tests: 39 still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,9 @@ namespace MarcusLaw.OutlookAddin.Core.Models
|
|||||||
[JsonPropertyName("folderId")]
|
[JsonPropertyName("folderId")]
|
||||||
public string? FolderId { get; set; }
|
public string? FolderId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("folderName")]
|
||||||
|
public string? FolderName { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("parentType")]
|
[JsonPropertyName("parentType")]
|
||||||
public string? ParentType { get; set; }
|
public string? ParentType { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,45 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<DocumentFolder>> ListFoldersUsedInCaseAsync(string caseId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(caseId)) return new List<DocumentFolder>();
|
||||||
|
|
||||||
|
// EspoCRM doesn't have a Case → DocumentFolder relationship by
|
||||||
|
// default — but it DOES expose all documents linked to the case
|
||||||
|
// via the related-records endpoint, and each document returns
|
||||||
|
// its folderId + folderName denormalized. We dedupe to surface
|
||||||
|
// the folders that are already in use for that case (the user's
|
||||||
|
// mental model: "תיקיות שיש בתיק").
|
||||||
|
var path =
|
||||||
|
"Case/" + Uri.EscapeDataString(caseId) + "/documents" +
|
||||||
|
"?select=" + Uri.EscapeDataString("id,folderId,folderName") +
|
||||||
|
"&maxSize=200";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await GetJsonAsync<EspoListResponse<DocumentEntity>>(path, cancellationToken).ConfigureAwait(false);
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
var folders = new List<DocumentFolder>();
|
||||||
|
foreach (var doc in response.List)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(doc.FolderId)) continue;
|
||||||
|
if (!seen.Add(doc.FolderId!)) continue;
|
||||||
|
folders.Add(new DocumentFolder
|
||||||
|
{
|
||||||
|
Id = doc.FolderId!,
|
||||||
|
Name = string.IsNullOrWhiteSpace(doc.FolderName) ? doc.FolderId! : doc.FolderName!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_logger.Information("ListFoldersUsedInCase({CaseId}) → {Count} distinct folders", caseId, folders.Count);
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex, "ListFoldersUsedInCaseAsync({CaseId}) failed", caseId);
|
||||||
|
return new List<DocumentFolder>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default)
|
public async Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
|||||||
Task<AttachmentEntity> UploadAttachmentAsync(string fileName, string contentType, string base64Content, CancellationToken cancellationToken = default);
|
Task<AttachmentEntity> UploadAttachmentAsync(string fileName, string contentType, string base64Content, CancellationToken cancellationToken = default);
|
||||||
Task<DocumentEntity> CreateDocumentAsync(string name, string attachmentFileId, string? parentType, string? parentId, string? folderId, CancellationToken cancellationToken = default);
|
Task<DocumentEntity> CreateDocumentAsync(string name, string attachmentFileId, string? parentType, string? parentId, string? folderId, CancellationToken cancellationToken = default);
|
||||||
Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default);
|
Task<EspoListResponse<DocumentFolder>> ListDocumentFoldersAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<System.Collections.Generic.List<DocumentFolder>> ListFoldersUsedInCaseAsync(string caseId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,22 +85,6 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadFoldersAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _client.ListDocumentFoldersAsync(ct).ConfigureAwait(true);
|
|
||||||
foreach (var folder in result.List)
|
|
||||||
{
|
|
||||||
Folders.Add(folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Warning(ex, "SaveAttachmentsViewModel.LoadFoldersAsync failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSearchTextChanged(string value)
|
partial void OnSearchTextChanged(string value)
|
||||||
{
|
{
|
||||||
_searchCts?.Cancel();
|
_searchCts?.Cancel();
|
||||||
@@ -111,6 +95,32 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
partial void OnSelectedCaseChanged(CaseEntity? value)
|
partial void OnSelectedCaseChanged(CaseEntity? value)
|
||||||
{
|
{
|
||||||
SubmitCommand.NotifyCanExecuteChanged();
|
SubmitCommand.NotifyCanExecuteChanged();
|
||||||
|
// Folders are scoped to the chosen case — refresh whenever the
|
||||||
|
// case selection changes.
|
||||||
|
_ = LoadFoldersForCaseAsync(value?.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadFoldersForCaseAsync(string? caseId)
|
||||||
|
{
|
||||||
|
// Reset to the sentinel row first; we'll append folders below.
|
||||||
|
Folders.Clear();
|
||||||
|
Folders.Add(new DocumentFolder { Id = string.Empty, Name = "(בלי תיקייה)" });
|
||||||
|
SelectedFolder = Folders[0];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(caseId)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var folders = await _client.ListFoldersUsedInCaseAsync(caseId!).ConfigureAwait(true);
|
||||||
|
foreach (var folder in folders)
|
||||||
|
{
|
||||||
|
Folders.Add(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warning(ex, "SaveAttachmentsViewModel.LoadFoldersForCaseAsync failed for {CaseId}", caseId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SearchAsync(string query, CancellationToken ct)
|
private async Task SearchAsync(string query, CancellationToken ct)
|
||||||
|
|||||||
@@ -482,8 +482,8 @@ namespace OutlookAddin.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var vm = new SaveAttachmentsViewModel(CrmClient!, Logger, envelope.Attachments);
|
var vm = new SaveAttachmentsViewModel(CrmClient!, Logger, envelope.Attachments);
|
||||||
// Best-effort folder list — failures already logged inside the VM.
|
// Folders are loaded lazily when the user picks a case — the
|
||||||
_ = vm.LoadFoldersAsync();
|
// sentinel "(בלי תיקייה)" row is already present.
|
||||||
|
|
||||||
var dialog = new SaveAttachmentsDialog(vm);
|
var dialog = new SaveAttachmentsDialog(vm);
|
||||||
AttachOwnerToOutlook(dialog);
|
AttachOwnerToOutlook(dialog);
|
||||||
|
|||||||
Reference in New Issue
Block a user