From ec45f0798371ee79c03da85444fe6230f9fdf84c Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Thu, 19 Mar 2026 18:45:21 -0700 Subject: [PATCH 1/6] AB#24672: Add Email Attachments Initial Draft --- .../Emails/EmailLogAttachmentDto.cs | 16 + .../Emails/IEmailLogAttachmentAppService.cs | 12 + .../IEmailLogAttachmentUploadService.cs | 9 + .../EmailNotificationManager.cs | 28 ++ .../EmailNotificationService.cs | 6 + .../IEmailNotificationManager.cs | 7 +- .../IEmailNotificationService.cs | 1 + .../Emails/EmailAttachmentService.cs | 67 +++- .../Emails/EmailLogAttachmentAppService.cs | 102 ++++++ .../Notifications/IEmailAppService.cs | 4 +- .../Norifications/EmailAppService.cs | 8 +- .../Controllers/AttachmentController.cs | 57 ++- .../Components/EmailHistoryWidget/Default.js | 1 + .../Components/EmailsWidget/Default.cshtml | 22 ++ .../Shared/Components/EmailsWidget/Default.js | 326 +++++++++++++++--- .../Components/AttachmentControllerTests.cs | 10 +- 16 files changed, 616 insertions(+), 60 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailLogAttachmentDto.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentAppService.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentUploadService.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailLogAttachmentAppService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailLogAttachmentDto.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailLogAttachmentDto.cs new file mode 100644 index 0000000000..fcccb3545d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailLogAttachmentDto.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.Notifications.Emails; + +[Serializable] +public class EmailLogAttachmentDto : EntityDto +{ + public string? FileName { get; set; } + public string? DisplayName { get; set; } + public DateTime Time { get; set; } + public long FileSize { get; set; } + public string ContentType { get; set; } = string.Empty; + public string S3ObjectKey { get; set; } = string.Empty; + public string AttachedBy { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentAppService.cs new file mode 100644 index 0000000000..bb9377063e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentAppService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.Notifications.Emails; + +public interface IEmailLogAttachmentAppService : IApplicationService +{ + Task> GetListByEmailLogIdAsync(Guid emailLogId); + Task DeleteAsync(Guid id); +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentUploadService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentUploadService.cs new file mode 100644 index 0000000000..23f2ae4f03 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/IEmailLogAttachmentUploadService.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.Notifications.Emails; + +public interface IEmailLogAttachmentUploadService +{ + Task UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType); +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs index eb031554d0..efc7bd98a0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs @@ -86,8 +86,36 @@ public class EmailNotificationManager( } } + public async Task CreateDraftEmailLogAsync(Guid applicationId) + { + var emailLog = new EmailLog + { + ApplicationId = applicationId, + Status = EmailStatus.Draft + }; + return await emailLogsRepository.InsertAsync(emailLog, autoSave: true); + } + public async Task DeleteEmailLogAsync(Guid id) { + var emailLog = await emailLogsRepository.GetAsync(id); + if (emailLog.Status == EmailStatus.Sent) + { + throw new UserFriendlyException("Sent emails cannot be deleted."); + } + + var attachments = await emailAttachmentService.GetAttachmentsAsync(id); + foreach (var attachment in attachments) + { + try + { + await emailAttachmentService.DeleteFromS3Async(attachment.S3ObjectKey); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete S3 attachment {S3ObjectKey} for EmailLog {EmailLogId}", attachment.S3ObjectKey, id); + } + } await emailLogsRepository.DeleteAsync(id); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index ffeb49a214..87881499f4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -31,6 +31,12 @@ public class EmailNotificationService( IFeatureChecker featureChecker) : ApplicationService, IEmailNotificationService { + public async Task InitializeDraftAsync(Guid applicationId) + { + var emailLog = await emailNotificationManager.CreateDraftEmailLogAsync(applicationId); + return emailLog.Id; + } + public async Task DeleteEmail(Guid id) { await emailNotificationManager.DeleteEmailLogAsync(id); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationManager.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationManager.cs index 19885e38e9..cc9fe5a31f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationManager.cs @@ -32,7 +32,12 @@ public interface IEmailNotificationManager Task GetEmailLogByIdAsync(Guid id); /// - /// Deletes an email log + /// Creates an empty draft email log for composing + /// + Task CreateDraftEmailLogAsync(Guid applicationId); + + /// + /// Deletes an email log and its S3 attachments /// Task DeleteEmailLogAsync(Guid id); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs index b009f53fb1..c598fef948 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs @@ -20,6 +20,7 @@ public interface IEmailNotificationService : IApplicationService Task SendEmailToQueue(EmailLog emailLog); Task> GetHistoryByApplicationId(Guid applicationId); Task UpdateSettings(NotificationsSettingsDto settingsDto); + Task InitializeDraftAsync(Guid applicationId); Task DeleteEmail(Guid id); Task GetEmailsChesWithNoResponseCountAsync(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs index 35068660d2..026c108998 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs @@ -111,11 +111,76 @@ public async Task UploadAttachmentAsync( return memoryStream.ToArray(); } + public async Task UploadUserAttachmentAsync( + Guid emailLogId, + Guid? tenantId, + string fileName, + byte[] fileContent, + string contentType) + { + var uniqueKey = Guid.NewGuid(); + var s3Key = BuildUserAttachmentS3Key(tenantId, emailLogId, uniqueKey, fileName); + var bucket = _configuration["S3:Bucket"]; + + using var uploadStream = new MemoryStream(fileContent); + var putRequest = new PutObjectRequest + { + BucketName = bucket, + Key = s3Key, + ContentType = contentType, + InputStream = uploadStream, + UseChunkEncoding = false, + DisablePayloadSigning = false + }; + + await _amazonS3Client.PutObjectAsync(putRequest); + _logger.LogInformation( + "Uploaded user email attachment to S3: FileName={FileName}, FileSize={FileSize}", + fileName, fileContent.Length); + + var attachment = new EmailLogAttachment + { + EmailLogId = emailLogId, + S3ObjectKey = s3Key, + FileName = fileName, + DisplayName = fileName, + ContentType = contentType, + FileSize = fileContent.Length, + Time = DateTime.UtcNow, + UserId = _currentUser.Id ?? Guid.Empty, + TenantId = tenantId + }; + + await _emailLogAttachmentRepository.InsertAsync(attachment); + return attachment; + } + + public async Task DeleteFromS3Async(string s3ObjectKey) + { + var bucket = _configuration["S3:Bucket"]; + var deleteRequest = new DeleteObjectRequest + { + BucketName = bucket, + Key = s3ObjectKey + }; + await _amazonS3Client.DeleteObjectAsync(deleteRequest); + _logger.LogInformation("Deleted email attachment from S3: Key={S3ObjectKey}", s3ObjectKey); + } + public async Task> GetAttachmentsAsync(Guid emailLogId) { return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); } + private static string BuildUserAttachmentS3Key(Guid? tenantId, Guid emailLogId, Guid attachmentId, string fileName) + { + var basePath = "Email/Attachments"; + var tenantPart = tenantId?.ToString() ?? "host"; + var escapedFileName = Uri.EscapeDataString(fileName); + + return $"{basePath}/{tenantPart}/{emailLogId}/{attachmentId}/{escapedFileName}"; + } + private static string BuildS3Key(Guid? tenantId, Guid emailLogId, string fileName) { var basePath = "Email/FSB-AP-Payments"; @@ -124,6 +189,4 @@ private static string BuildS3Key(Guid? tenantId, Guid emailLogId, string fileNam return $"{basePath}/{tenantPart}/{emailLogId}/{escapedFileName}"; } - - } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailLogAttachmentAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailLogAttachmentAppService.cs new file mode 100644 index 0000000000..6e4e09e089 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailLogAttachmentAppService.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.Notifications.EmailNotifications; +using Unity.Notifications.Permissions; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Users; + +namespace Unity.Notifications.Emails; + +[Authorize(NotificationsPermissions.Email.Send)] +[ExposeServices(typeof(EmailLogAttachmentAppService), typeof(IEmailLogAttachmentAppService), typeof(IEmailLogAttachmentUploadService))] +public class EmailLogAttachmentAppService( + IEmailLogAttachmentRepository emailLogAttachmentRepository, + IEmailLogsRepository emailLogsRepository, + EmailAttachmentService emailAttachmentService, + IExternalUserLookupServiceProvider externalUserLookupServiceProvider) : ApplicationService, IEmailLogAttachmentAppService, IEmailLogAttachmentUploadService +{ + public async Task> GetListByEmailLogIdAsync(Guid emailLogId) + { + var attachments = await emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); + var dtos = new List(); + + foreach (var attachment in attachments) + { + var dto = new EmailLogAttachmentDto + { + Id = attachment.Id, + FileName = attachment.FileName, + DisplayName = attachment.DisplayName, + Time = attachment.Time, + FileSize = attachment.FileSize, + ContentType = attachment.ContentType, + S3ObjectKey = attachment.S3ObjectKey, + AttachedBy = await ResolveUserNameAsync(attachment.UserId) + }; + dtos.Add(dto); + } + + return dtos; + } + + public async Task DeleteAsync(Guid id) + { + var attachment = await emailLogAttachmentRepository.GetAsync(id); + + var emailLog = await emailLogsRepository.GetAsync(attachment.EmailLogId); + if (emailLog.Status != EmailStatus.Draft) + { + throw new UserFriendlyException("Attachments can only be deleted from draft emails."); + } + + try + { + await emailAttachmentService.DeleteFromS3Async(attachment.S3ObjectKey); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete S3 object {S3ObjectKey} for attachment {AttachmentId}", attachment.S3ObjectKey, id); + } + await emailLogAttachmentRepository.DeleteAsync(id); + } + + public async Task UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType) + { + var attachment = await emailAttachmentService.UploadUserAttachmentAsync(emailLogId, tenantId, fileName, content, contentType); + + return new EmailLogAttachmentDto + { + Id = attachment.Id, + FileName = attachment.FileName, + DisplayName = attachment.DisplayName, + Time = attachment.Time, + FileSize = attachment.FileSize, + ContentType = attachment.ContentType, + S3ObjectKey = attachment.S3ObjectKey, + AttachedBy = await ResolveUserNameAsync(attachment.UserId) + }; + } + + private async Task ResolveUserNameAsync(Guid userId) + { + try + { + var user = await externalUserLookupServiceProvider.FindByIdAsync(userId); + if (user == null) return string.Empty; + + var fullName = $"{user.Name} {user.Surname}".Trim(); + return string.IsNullOrEmpty(fullName) ? user.UserName : fullName; + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to resolve username for UserId {UserId}", userId); + return string.Empty; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs index 0c6b77a823..37ef649a82 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs @@ -1,9 +1,11 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Unity.GrantManager.Emails { public interface IEmailAppService { Task CreateAsync(CreateEmailDto dto); + Task InitializeDraftAsync(Guid applicationId); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs index cfd04e2604..7e8ad607fd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Authorization; +using System; using System.Threading.Tasks; using Unity.Modules.Shared.Utils; +using Unity.Notifications.EmailNotifications; using Unity.Notifications.Emails; using Unity.Notifications.Events; using Volo.Abp.Application.Services; @@ -12,8 +14,12 @@ namespace Unity.GrantManager.Emails [Authorize] [Dependency(ReplaceServices = true)] [ExposeServices(typeof(EmailAppService), typeof(IEmailAppService))] - public class EmailAppService(ILocalEventBus localEventBus) : ApplicationService, IEmailAppService + public class EmailAppService(ILocalEventBus localEventBus, IEmailNotificationService emailNotificationService) : ApplicationService, IEmailAppService { + public async Task InitializeDraftAsync(Guid applicationId) + { + return await emailNotificationService.InitializeDraftAsync(applicationId); + } public async Task CreateAsync(CreateEmailDto dto) { EmailNotificationEvent emailNotificationEvent = GetEmailNotificationEvent(dto); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/AttachmentController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/AttachmentController.cs index 4941e3c31f..42f004ef31 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/AttachmentController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/AttachmentController.cs @@ -10,7 +10,9 @@ using System.Threading.Tasks; using Unity.GrantManager.Attachments; using Unity.GrantManager.Intakes; +using Unity.Notifications.Emails; using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.MultiTenancy; using Volo.Abp.Validation; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -24,17 +26,26 @@ public class AttachmentController : AbpController private readonly IFileAppService _fileAppService; private readonly IConfiguration _configuration; private readonly ISubmissionAppService _submissionAppService; + private readonly IEmailLogAttachmentUploadService _emailLogAttachmentUploadService; + private readonly ICurrentTenant _currentTenant; private ILogger logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); private const string badRequestFileMsg = "File name must be provided."; private const string NotFoundFileMsg = "File not found."; private const string errorFileMsg = "An error occurred while downloading the file."; private const string chefsApiAccessError = "You do not have access to this resource"; - public AttachmentController(IFileAppService fileAppService, IConfiguration configuration, ISubmissionAppService submissionAppService) + public AttachmentController( + IFileAppService fileAppService, + IConfiguration configuration, + ISubmissionAppService submissionAppService, + IEmailLogAttachmentUploadService emailLogAttachmentUploadService, + ICurrentTenant currentTenant) { _fileAppService = fileAppService; _configuration = configuration; _submissionAppService = submissionAppService; + _emailLogAttachmentUploadService = emailLogAttachmentUploadService; + _currentTenant = currentTenant; } [HttpGet("application/{applicationId}/download/{fileName}")] @@ -250,6 +261,50 @@ public async Task UploadApplicationAttachments(Guid applicationId return await UploadFiles(files); } + [HttpPost("email/{emailLogId}/upload")] + public async Task UploadEmailAttachments(Guid emailLogId, IList files) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (files == null || files.Count == 0) + { + return BadRequest("At least one file must be provided."); + } + + List invalidFileTypes = GetInvalidFileTypes(files); + if (invalidFileTypes.Count > 0) + { + throw new AbpValidationException(message: "ERROR: Invalid File Type.", validationErrors: invalidFileTypes); + } + + var results = new List(); + foreach (var file in files) + { + try + { + using var ms = new MemoryStream(); + await file.CopyToAsync(ms); + var dto = await _emailLogAttachmentUploadService.UploadAsync( + emailLogId, + _currentTenant.Id, + file.FileName, + ms.ToArray(), + file.ContentType ?? "application/octet-stream"); + results.Add(dto); + } + catch (Exception ex) + { + logger.LogError(ex, "AttachmentController->UploadEmailAttachments: Failed to upload {FileName}", file.FileName); + return StatusCode(500, $"Failed to upload {file.FileName}: {ex.Message}"); + } + } + + return Ok(results); + } + private async Task UploadFiles(IList files) { List InvalidFileTypes = GetInvalidFileTypes(files); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js index 36d9b92c77..a6de4c4a82 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js @@ -177,6 +177,7 @@ function deleteDraftEmail(id, rowIndex) { .then(response => { abp.notify.success('Draft email is successfully deleted.', 'Delete Draft Email'); PubSub.publish('refresh_application_emails'); + PubSub.publish('draft_email_deleted', { id: id }); }) .catch(error => { console.error('There was a problem with the fetch operation:', error); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml index 3627fab784..e050edc3ae 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml @@ -118,6 +118,28 @@ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index 79fa979834..92a1fae74f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -38,6 +38,9 @@ let applicationDetails; let mappingConfig; let editorInstance; + let isNewEmailDraft = false; + let newDraftId = null; + let emailAttachmentsTable = null; function bindUIEvents() { UIElements.btnNewEmail.on('click', handleNewEmail); UIElements.btnSend.on('click', handleSendEmail); @@ -110,15 +113,26 @@ e.currentTarget.value = trimmedString; } - function handleCloseEmail() { + function closeEmailFormUI() { $('#modal-content, #modal-background').removeClass('active'); UIElements.emailForm.removeClass('active'); UIElements.btnNewEmail.removeClass('hide'); UIElements.alertEmailReadonly.removeClass('hide'); UIElements.emailForm.trigger("reset"); + $('#email-attachments-section').hide(); enableEmail(); } + function handleCloseEmail() { + if (isNewEmailDraft && newDraftId) { + $.ajax({ url: `/api/app/email-notification/${newDraftId}/email`, type: 'DELETE' }) + .catch(e => console.warn('Failed to delete draft on close:', e)); + isNewEmailDraft = false; newDraftId = null; + newDraftId = null; + } + closeEmailFormUI(); + } + function handleDiscardEmail() { UIElements.inputEmailTo.val(UIElements.inputOriginalEmailTo.val()); UIElements.inputEmailCC.val(UIElements.inputOriginalEmailCC.val()); @@ -150,21 +164,47 @@ // 2. Clear the underlying