From 5037ba24caf595a46363b85b2975f6259d3544dc Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 3 Jul 2026 13:14:55 -0700 Subject: [PATCH 1/9] feature/AB#32580-TemplateAttachments --- .../Emails/IEmailLogAttachmentAppService.cs | 1 + .../IEmailLogAttachmentUploadService.cs | 4 +- .../Emails/EmailAttachmentService.cs | 32 +- .../Emails/EmailLogAttachmentAppService.cs | 49 +- .../Events/EmailNotificationHandler.cs | 1 + .../Templates/ITemplatesService.cs | 2 +- .../Templates/TemplatesService.cs | 2 +- .../Emails/EmailLogAttachment.cs | 3 +- .../Emails/IEmailLogAttachmentRepository.cs | 1 + ...cationsDbContextModelCreatingExtensions.cs | 8 + .../EmailLogAttachmentRepository.cs | 12 +- .../NotificationsSettingGroup/Default.cshtml | 15 +- .../NotificationsSettingGroup/Default.css | 212 +- .../NotificationsSettingGroup/Default.js | 1226 ++-- .../_TemplateDetails.cshtml | 52 + .../_Templates.cshtml | 42 + .../DateBasedScheduledNotificationJob.cs | 2 +- .../ApplicationLinksAppService.cs | 6 +- ...24175515_AlterEmailAttachments.Designer.cs | 5227 +++++++++++++++++ .../20260624175515_AlterEmailAttachments.cs | 98 + .../GrantTenantDbContextModelSnapshot.cs | 3 + .../Controllers/AttachmentController.cs | 50 +- .../FormNotificationsApiController.cs | 22 +- .../ConfigurationManagement/Index.cshtml | 4 - .../Pages/ConfigurationManagement/Index.css | 3 +- .../Pages/ConfigurationManagement/Index.js | 25 +- .../Components/EmailsWidget/Default.cshtml | 36 +- .../Shared/Components/EmailsWidget/Default.js | 15 +- .../EmailsWidget/EmailsWidgetViewComponent.cs | 2 +- .../EmailsWidget/_EmailAttachments.cshtml | 34 + .../Components/Notifications/Default.js | 20 +- 31 files changed, 6653 insertions(+), 556 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_TemplateDetails.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_Templates.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260624175515_AlterEmailAttachments.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260624175515_AlterEmailAttachments.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/_EmailAttachments.cshtml 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 index bb9377063e..9efab060ab 100644 --- 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 @@ -8,5 +8,6 @@ namespace Unity.Notifications.Emails; public interface IEmailLogAttachmentAppService : IApplicationService { Task> GetListByEmailLogIdAsync(Guid emailLogId); + Task> GetListByTemplateIdAsync(Guid templateId); 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 index cdea764cb9..93a4e3588f 100644 --- 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 @@ -5,6 +5,6 @@ namespace Unity.Notifications.Emails; public interface IEmailLogAttachmentUploadService { - Task UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType); - Task GetTotalFileSizeByEmailLogIdAsync(Guid emailLogId); + Task UploadAsync(Guid? emailLogId, Guid? templateId, Guid? tenantId, string fileName, byte[] content, string contentType); + Task GetTotalFileSizeByEmailLogIdAsync(Guid? emailLogId, Guid? templateId); } 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 03793d3383..42c22f5d17 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 @@ -38,13 +38,15 @@ public EmailAttachmentService( } public async Task UploadAttachmentAsync( - Guid emailLogId, + Guid? emailLogId, + Guid? templateId, Guid? tenantId, string fileName, byte[] fileContent, string contentType) { - var s3Key = BuildS3Key(tenantId, emailLogId, fileName); + var guid = emailLogId ?? templateId ?? throw new ArgumentException("Either emailLogId or templateId must be provided."); + var s3Key = BuildS3Key(tenantId, guid, fileName); var bucket = _configuration[S3BucketConfigKey]; // Upload to S3 @@ -68,6 +70,7 @@ public async Task UploadAttachmentAsync( var attachment = new EmailLogAttachment { EmailLogId = emailLogId, + TemplateId = templateId, S3ObjectKey = s3Key, FileName = fileName, DisplayName = fileName, @@ -102,14 +105,16 @@ public async Task UploadAttachmentAsync( } public async Task UploadUserAttachmentAsync( - Guid emailLogId, + Guid? emailLogId, + Guid? templateId, Guid? tenantId, string fileName, byte[] fileContent, string contentType) { var uniqueKey = Guid.NewGuid(); - var s3Key = BuildUserAttachmentS3Key(tenantId, emailLogId, uniqueKey, fileName); + Guid generateGuid = emailLogId ?? templateId ?? throw new ArgumentException("Either emailLogId or templateId must be provided."); + var s3Key = BuildUserAttachmentS3Key(tenantId, generateGuid, uniqueKey, fileName); var bucket = _configuration[S3BucketConfigKey]; using var uploadStream = new MemoryStream(fileContent); @@ -131,6 +136,7 @@ public async Task UploadUserAttachmentAsync( var attachment = new EmailLogAttachment { EmailLogId = emailLogId, + TemplateId = templateId, S3ObjectKey = s3Key, FileName = fileName, DisplayName = fileName, @@ -162,10 +168,22 @@ public async Task> GetAttachmentsAsync(Guid emailLogId) return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); } - public async Task GetTotalFileSizeAsync(Guid emailLogId) + public async Task GetTotalFileSizeAsync(Guid? emailLogId, Guid? templateId) { - var attachments = await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); - return attachments.Sum(a => a.FileSize); + if(emailLogId != null) + { + var attachments = await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId.Value); + return attachments.Sum(a => a.FileSize); + } + else if(templateId != null) + { + var attachments = await _emailLogAttachmentRepository.GetByTemplateIdAsync(templateId.Value); + return attachments.Sum(a => a.FileSize); + } + else + { + throw new ArgumentException("Either emailLogId or templateId must be provided."); + } } private static string BuildUserAttachmentS3Key(Guid? tenantId, Guid emailLogId, Guid attachmentId, string fileName) 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 index 31a5a9b20b..e69f253077 100644 --- 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 @@ -20,9 +20,29 @@ public class EmailLogAttachmentAppService( EmailAttachmentService emailAttachmentService, IExternalUserLookupServiceProvider externalUserLookupServiceProvider) : ApplicationService, IEmailLogAttachmentAppService, IEmailLogAttachmentUploadService { + public async Task> GetListByEmailLogIdAsync(Guid emailLogId) { - var attachments = await emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); + return await GetListBydAsync(emailLogId, null); + } + + public async Task> GetListByTemplateIdAsync(Guid templateId) + { + return await GetListBydAsync(null, templateId); + } + + public async Task> GetListBydAsync(Guid? emailLogId, Guid? templateId) + { + var attachments = new List(); + if (emailLogId.HasValue) + { + attachments = await emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId.Value); + } + else if (templateId.HasValue) + { + attachments = await emailLogAttachmentRepository.GetByTemplateIdAsync(templateId.Value); + } + var dtos = new List(); foreach (var attachment in attachments) @@ -47,7 +67,24 @@ public async Task DeleteAsync(Guid id) { var attachment = await emailLogAttachmentRepository.GetAsync(id); - var emailLog = await emailLogsRepository.GetAsync(attachment.EmailLogId); + if (attachment == null) + { + throw new UserFriendlyException("Attachment not found."); + } + + if (attachment.TemplateId.HasValue) + { + await emailAttachmentService.DeleteFromS3Async(attachment.S3ObjectKey); + await emailLogAttachmentRepository.DeleteAsync(id); + return; + } + + if(attachment.EmailLogId == null) + { + throw new UserFriendlyException("Invalid email log ID."); + } + + var emailLog = await emailLogsRepository.GetAsync(attachment.EmailLogId.Value); if (emailLog.Status != EmailStatus.Draft) { throw new UserFriendlyException("Attachments can only be deleted from draft emails."); @@ -64,14 +101,14 @@ public async Task DeleteAsync(Guid id) await emailLogAttachmentRepository.DeleteAsync(id); } - public async Task GetTotalFileSizeByEmailLogIdAsync(Guid emailLogId) + public async Task GetTotalFileSizeByEmailLogIdAsync(Guid? emailLogId, Guid? templateId) { - return await emailAttachmentService.GetTotalFileSizeAsync(emailLogId); + return await emailAttachmentService.GetTotalFileSizeAsync(emailLogId, templateId); } - public async Task UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType) + public async Task UploadAsync(Guid? emailLogId, Guid? templateId, Guid? tenantId, string fileName, byte[] content, string contentType) { - var attachment = await emailAttachmentService.UploadUserAttachmentAsync(emailLogId, tenantId, fileName, content, contentType); + var attachment = await emailAttachmentService.UploadUserAttachmentAsync(emailLogId, templateId, tenantId, fileName, content, contentType); return new EmailLogAttachmentDto { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs index 4de2379eee..580deb3c88 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs @@ -116,6 +116,7 @@ private async Task InitializeEmailAndUploadAttachments(EmailInitParams { await emailAttachmentService.UploadAttachmentAsync( emailLog.Id, + null, // No templateId for user-uploaded attachments emailLog.TenantId, attachmentData.FileName, attachmentData.Content, diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/ITemplatesService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/ITemplatesService.cs index 33dec9a293..b6f24d28a9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/ITemplatesService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/ITemplatesService.cs @@ -9,7 +9,7 @@ public interface ITemplateService : IApplicationService { Task CreateAsync(EmailTempateDto templateDto); Task UpdateTemplate(Guid id, EmailTempateDto templateDto); - Task> GetTemplatesByTenent(); + Task> GetTemplatesByTenant(); Task GetTemplateById(Guid id); Task DeleteTemplate(Guid id); Task GetTemplateByName(string name); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs index ee51827d0f..cdf6a6b354 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs @@ -60,7 +60,7 @@ ITemplateVariablesRepository templateVariablesRepository return updatedTemplate; } - public async Task> GetTemplatesByTenent() + public async Task> GetTemplatesByTenant() { var tenentId = _currentTenant.Id; return await _templatesRepository.GetByTenentIdAsync(tenentId); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLogAttachment.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLogAttachment.cs index 80db1b7ded..e617cfc77b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLogAttachment.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLogAttachment.cs @@ -8,7 +8,8 @@ namespace Unity.Notifications.Emails; public class EmailLogAttachment : AuditedAggregateRoot, IMultiTenant { // Foreign key to EmailLog - public Guid EmailLogId { get; set; } + public Guid? EmailLogId { get; set; } + public Guid? TemplateId { get; set; } // S3 storage properties public string S3ObjectKey { get; set; } = string.Empty; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogAttachmentRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogAttachmentRepository.cs index c265eff986..9e48798cf4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogAttachmentRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogAttachmentRepository.cs @@ -8,4 +8,5 @@ namespace Unity.Notifications.Emails; public interface IEmailLogAttachmentRepository : IBasicRepository { Task> GetByEmailLogIdAsync(Guid emailLogId); + Task> GetByTemplateIdAsync(Guid templateId); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs index d38c2e6713..8898ae6a8a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs @@ -35,10 +35,18 @@ public static void ConfigureNotifications( // Foreign key to EmailLog with CASCADE delete b.HasOne() .WithMany() + .IsRequired(false) .HasForeignKey(x => x.EmailLogId) .OnDelete(DeleteBehavior.Cascade); + b.HasOne() + .WithMany() + .IsRequired(false) + .HasForeignKey(x => x.TemplateId) + .OnDelete(DeleteBehavior.Cascade); + // Indexes for performance + b.HasIndex(x => x.TemplateId); b.HasIndex(x => x.EmailLogId); b.HasIndex(x => x.S3ObjectKey); }); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogAttachmentRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogAttachmentRepository.cs index d8b1a13986..766db880aa 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogAttachmentRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogAttachmentRepository.cs @@ -10,18 +10,20 @@ namespace Unity.Notifications.Repositories { - public class EmailLogAttachmentRepository : EfCoreRepository, + public class EmailLogAttachmentRepository(IDbContextProvider dbContextProvider) : + EfCoreRepository(dbContextProvider), IEmailLogAttachmentRepository { - public EmailLogAttachmentRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) + public async Task> GetByEmailLogIdAsync(Guid emailLogId) { + var dbSet = await GetDbSetAsync(); + return await dbSet.Where(x => x.EmailLogId == emailLogId).ToListAsync(); } - public async Task> GetByEmailLogIdAsync(Guid emailLogId) + public async Task> GetByTemplateIdAsync(Guid templateId) { var dbSet = await GetDbSetAsync(); - return await dbSet.Where(x => x.EmailLogId == emailLogId).ToListAsync(); + return await dbSet.Where(x => x.TemplateId == templateId).ToListAsync(); } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml index de95d9181a..bdc9957838 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml @@ -1,10 +1,6 @@ @model Unity.Notifications.Web.Views.Settings.NotificationsSettingGroup.NotificationsSettingViewModel
-
-

Notifications

-
-