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 a68c3109c..0eadcb2df 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 @@ -178,6 +178,63 @@ public async Task> GetAttachmentsAsync(Guid emailLogId) return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); } + public async Task CopyTemplateAttachmentsAsync(Guid templateId, Guid emailLogId, Guid? tenantId) + { + var templateAttachments = await _emailLogAttachmentRepository.GetByTemplateIdAsync(templateId); + var existingAttachments = await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); + // Dedup by (FileName, FileSize, ContentType) rather than S3ObjectKey: each copy gets its own + // S3 object (see below), so a re-run of this method for the same emailLogId/templateId would + // never see a matching key even though the attachment was already copied. + var alreadyCopied = existingAttachments + .Where(a => a.OriginTemplateId == templateId) + .Select(a => (a.FileName, a.FileSize, a.ContentType)) + .ToHashSet(); + + var bucket = _configuration[S3BucketConfigKey]; + var copiedAttachmentCount = 0; + foreach (var templateAttachment in templateAttachments) + { + var identity = (templateAttachment.FileName, templateAttachment.FileSize, templateAttachment.ContentType); + if (!alreadyCopied.Add(identity)) + { + continue; + } + + // Physically duplicate the S3 object under a new key instead of pointing at the + // template attachment's own key. EmailLogAttachmentAppService.DeleteAsync deletes the + // underlying S3 object whenever a template attachment (TemplateId.HasValue) is removed; + // sharing the key would silently break the attachment on every scheduled email that had + // already copied it. + var copiedS3Key = BuildUserAttachmentS3Key( + tenantId, emailLogId, Guid.NewGuid(), templateAttachment.FileName ?? templateAttachment.DisplayName ?? "attachment"); + await _amazonS3Client.CopyObjectAsync(new CopyObjectRequest + { + SourceBucket = bucket, + SourceKey = templateAttachment.S3ObjectKey, + DestinationBucket = bucket, + DestinationKey = copiedS3Key + }); + + await _emailLogAttachmentRepository.InsertAsync(new EmailLogAttachment + { + EmailLogId = emailLogId, + TemplateId = null, + OriginTemplateId = templateId, + S3ObjectKey = copiedS3Key, + FileName = templateAttachment.FileName, + DisplayName = templateAttachment.DisplayName, + ContentType = templateAttachment.ContentType, + FileSize = templateAttachment.FileSize, + Time = DateTime.UtcNow, + UserId = Guid.Empty, + TenantId = tenantId + }); + copiedAttachmentCount++; + } + + return copiedAttachmentCount; + } + public async Task GetTotalFileSizeAsync(Guid? emailLogId, Guid? templateId) { if(emailLogId != null) 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 580deb3c8..05be5543b 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 @@ -229,6 +229,26 @@ private async Task InitializeEmail(EmailInitParams p, string status) emailLog.ScheduledNotificationId = eventData.ScheduledNotificationId.Value; await emailLogsRepository.UpdateAsync(emailLog, autoSave: true); } + + if (eventData.ScheduledNotificationId.HasValue && eventData.TemplateId != Guid.Empty) + { + try + { + var copiedAttachmentCount = await emailAttachmentService.CopyTemplateAttachmentsAsync( + eventData.TemplateId, emailLog.Id, emailLog.TenantId); + _logger.LogInformation( + "Copied {AttachmentCount} template attachments for scheduled notification {ScheduledNotificationId}.", + copiedAttachmentCount, eventData.ScheduledNotificationId.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to copy template attachments for scheduled notification {ScheduledNotificationId}. Email will be sent WITHOUT attachments.", + eventData.ScheduledNotificationId.Value); + // DO NOT THROW - matches InitializeEmailAndUploadAttachments: an attachment + // failure should not block the email from being created/sent. + } + } await StampClassificationAsync(emailLog); return emailLog; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css index 0f18cea08..a08da5ac7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css @@ -189,6 +189,10 @@ body { justify-content: center; } +.split-container.split-active .left-pane { + flex: 0 0 48%; +} + /* ── Right pane ───────────────────────────────────────────────────────────── */ .right-pane { flex-shrink: 0; @@ -205,6 +209,19 @@ body { height: calc(100vh - 230px); } +.right-pane > .card { + min-height: 0; +} + +.right-pane .editor-body { + overflow-x: hidden; + overflow-y: auto; +} + +#email-attachments-section { + flex-shrink: 0; +} + /* ── Editor header ────────────────────────────────────────────────── */ .editor-header { @@ -222,6 +239,15 @@ body { padding: 2px; } +#email-attachments-section .d-flex.justify-content-end.mt-2.mb-1 { + padding-bottom: 50px !important; + display: flex !important; +} + +.right-pane .card-body.editor-body.overflow-auto { + padding-bottom: 20px !important; +} + /* ── Resizable textareas ──────────────────────────────────────────────────── */ .textarea { resize: vertical; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js index d62a88a5d..80346cc47 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js @@ -29,6 +29,7 @@ $(function () { let emailAttachmentsTable = null; let templatesDataTable = null; let originalFormValues = {}; + let attachmentChangesPending = false; function init() { $('#email-attachments-section').hide(); @@ -296,6 +297,7 @@ $(function () { UiElements.deleteButton.show(); $('#email-attachments-section').show(); + attachmentChangesPending = false; initEmailAttachmentsTable(data.id); // Recalculate table columns after initialization @@ -328,6 +330,7 @@ $(function () { $('#templateRecipientSelect').empty().val([]).trigger('change'); UiElements.deleteButton.hide(); $('#email-attachments-section').hide(); + attachmentChangesPending = false; // Don't load attachments for new templates - they have no ID yet } @@ -424,6 +427,7 @@ $(function () { }; const isNewTemplate = !templateId || templateId.trim() === ''; + const templateChangesPending = hasTemplateChanges(templateData) || attachmentChangesPending; // Check template name uniqueness before saving checkTemplateNameUnique(templateName.trim(), templateId, function (isUnique) { @@ -431,10 +435,56 @@ $(function () { markFieldError('templateName', 'Template name must be unique.'); return; } - performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML); + confirmTemplateAttachmentImpact(templateId, isNewTemplate, templateChangesPending) + .then(function (confirmed) { + if (confirmed) { + performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML); + } + }); }); }); + function hasTemplateChanges(templateData) { + const original = originalFormValues; + const fields = ['name', 'description', 'sendFrom', 'subject', 'bodyText', 'bodyHTML', 'recipientCategory', 'recipientIdentifier']; + + return fields.some(field => String(templateData[field] ?? '') !== String(original[field] ?? '')); + } + + function confirmTemplateAttachmentImpact(templateId, isNewTemplate, templateChangesPending) { + if (isNewTemplate || !templateChangesPending) { + return Promise.resolve(true); + } + + return $.ajax({ + url: `/api/form-notifications/template-notification-plans/${encodeURIComponent(templateId)}`, + type: 'GET', + dataType: 'json' + }).then(function (response) { + const planNames = response.notificationPlanNames || []; + if (planNames.length === 0) { + return true; + } + + return Swal.fire({ + icon: 'warning', + title: 'Template changes', + html: '

Warning: This template is currently associated with ' + planNames.length + ' notification plan' + (planNames.length === 1 ? '' : 's') + '. Any changes made to this template may impact these notification plan' + (planNames.length === 1 ? '' : 's') + '.

', + showCancelButton: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then(result => result.isConfirmed); + }).catch(function (e) { + console.warn('Failed to check template notification plans:', e); + abp.notify.error('Unable to verify whether this template is used by a notification plan. The template was not saved.'); + return false; + }); + } + function performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML) { if (isNewTemplate) { // Create new template @@ -472,6 +522,7 @@ $(function () { unity.notifications.templates.template .updateTemplate(templateId, templateData) .then(function () { + attachmentChangesPending = false; abp.notify.success('Template updated successfully.'); // Update original values after successful save originalFormValues = { @@ -481,7 +532,9 @@ $(function () { sendFrom: sendFrom, subject: subject, bodyText: '', - bodyHTML: bodyHTML + bodyHTML: bodyHTML, + recipientCategory: templateData.recipientCategory || '', + recipientIdentifier: templateData.recipientIdentifier || '' }; PubSub.publish('reload_templates_table_no_close'); }) @@ -969,6 +1022,7 @@ $(function () { $('#attachment-upload-progress').show(); }, success: function () { + attachmentChangesPending = true; PubSub.publish('reload_email_attachments_table'); }, error: function (xhr) { @@ -1047,6 +1101,10 @@ $(function () { reloadEmailAttachmentsTable(); }); + PubSub.subscribe('template_attachment_changed', () => { + attachmentChangesPending = true; + }); + function reloadEmailAttachmentsTable() { if (emailAttachmentsTable) { emailAttachmentsTable.ajax.reload(); @@ -1162,14 +1220,24 @@ function generateEmailAttachmentButtonContent(attachmentId) { * @param {string} attachmentId - Attachment ID to delete */ function deleteEmailAttachment(attachmentId) { - abp.message.confirm( - 'Are you sure you want to delete this attachment?', - 'Delete Attachment', - function (confirmed) { - if (confirmed) { + const templateId = $('#templateId').val(); + const planImpactCheck = isConfigurationManagementTemplateEditor() + ? checkScheduledPlanImpactForAttachmentDelete(templateId) + : Promise.resolve(true); + + planImpactCheck.then(function (confirmed) { + if (!confirmed) return; + + abp.message.confirm( + 'Are you sure you want to delete this attachment?', + 'Delete Attachment', + function (deleteConfirmed) { + if (!deleteConfirmed) return; + unity.notifications.emails.emailLogAttachment .delete(attachmentId) .then(function () { + PubSub.publish('template_attachment_changed'); abp.notify.success('Attachment deleted successfully.'); PubSub.publish('reload_email_attachments_table'); }) @@ -1178,8 +1246,45 @@ function deleteEmailAttachment(attachmentId) { abp.notify.error('Failed to delete attachment.'); }); } - } - ); + ); + }); +} + +function isConfigurationManagementTemplateEditor() { + return window.location.pathname.toLowerCase() === '/configurationmanagement' && + $('#nav-template').length > 0 && + $('#TemplatesTable').length > 0 && + $('#templateId').length > 0; +} + +function checkScheduledPlanImpactForAttachmentDelete(templateId) { + if (!templateId) return Promise.resolve(true); + + return $.ajax({ + url: `/api/form-notifications/template-notification-plans/${encodeURIComponent(templateId)}`, + type: 'GET', + dataType: 'json' + }).then(function (response) { + const planNames = response.notificationPlanNames || []; + if (planNames.length === 0) return true; + + return Swal.fire({ + icon: 'warning', + title: 'Scheduled notification impact', + html: '

Warning: This template is currently associated with ' + planNames.length + ' notification plan' + (planNames.length === 1 ? '' : 's') + '. Any changes made to this template may impact these notification plan' + (planNames.length === 1 ? '' : 's') + '.

', + showCancelButton: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then(result => result.isConfirmed); + }).catch(function (e) { + console.warn('Failed to check template notification plans:', e); + abp.notify.error('Unable to verify whether this template is used by a scheduled notification plan. The attachment was not deleted.'); + return false; + }); } /** diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs index 7e6533d63..200fe8a01 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs @@ -364,18 +364,47 @@ public async Task> CanDeleteTemplate(Guid templateId) var result = await _automatedNotificationAppService.GetListAsync( new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); - var inUse = result.Items.Any(n => n.EmailTemplateId == templateId); + var associatedPlans = result.Items + .Where(n => n.EmailTemplateId == templateId) + .Select(n => + string.IsNullOrWhiteSpace(n.TriggerDetail) + ? $"{n.TriggerType} notification" + : $"{n.TriggerType} notification - {n.TriggerDetail}") + .Distinct() + .ToList(); + + var inUse = associatedPlans.Count > 0; if (inUse) { return Ok(new { canDelete = false, - errorMessage = "This template cannot be deleted because it is assigned to one or more Scheduled Notifications. Please remove the template from all Scheduled Notifications before deleting." + errorMessage = "This template cannot be deleted because it is assigned to one or more Scheduled Notifications. Please remove the template from all Scheduled Notifications before deleting.", + notificationPlanNames = associatedPlans }); } - return Ok(new { canDelete = true, errorMessage = (string?)null }); + return Ok(new { canDelete = true, errorMessage = (string?)null, notificationPlanNames = Array.Empty() }); + } + + [HttpGet("template-notification-plans/{templateId:guid}")] + public async Task> GetTemplateNotificationPlans(Guid templateId) + { + var result = await _automatedNotificationAppService.GetListAsync( + new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); + var template = await _templateService.GetTemplateById(templateId); + + var templateName = template?.Name ?? "Template"; + var planNames = result.Items + .Where(n => n.IsActive && n.EmailTemplateId == templateId) + .Select(n => string.IsNullOrWhiteSpace(n.TriggerDetail) + ? $"{templateName} - {n.TriggerType} notification" + : $"{templateName} - {n.TriggerType} notification - {n.TriggerDetail}") + .Distinct() + .ToList(); + + return Ok(new { notificationPlanNames = planNames }); } [HttpPut("{formId}/{id:guid}")] diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml index 15e47c29f..130e0e221 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml @@ -134,7 +134,6 @@
- @* Placeholder for any extra widgets *@ @await Component.InvokeAsync("Notifications", new { formid = Model.FormId })
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml index 2cbf3eb2a..d535f1f2f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml @@ -122,11 +122,27 @@
+
Attachments (0)
+
+ + + + + + + + + +
Document NameDateAttached byFile Size
+
+
+ Note: Email templates and attachments cannot be edited here. To make changes, update the selected template in Configuration Management. +
Note: If Recipients are not found, then no email will be drafted or sent.
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css index 2b5c676da..84c4bdd44 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css @@ -4,15 +4,6 @@ background: #f8f9fb; font-family: var(--bs-body-font-family, 'BCSans', sans-serif); font-size: var(--bc-font-size, 1rem); - display: flex; - flex: 1 1 auto; - flex-direction: column; - height: 100%; - width: 100%; - min-width: 0; - min-height: 0; - position: relative; - overflow: hidden; } .notifications-widget .card { border: 0; } .notifications-widget .card .card-body { background: #fff; } @@ -90,11 +81,11 @@ min-width: 0; } -#notificationForm { - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; +#notificationForm { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; } /* Modal styling */ @@ -108,7 +99,8 @@ } .notification-modal-body { - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; flex: 1 1 auto; display: flex; flex-direction: column; @@ -116,7 +108,15 @@ .notification-modal-footer { flex-shrink: 0; - background-color: #f8f9fb; +} + +#notificationModal .notification-modal-footer { + padding-bottom: 0.67rem; +} + + +#notificationModal .modal-dialog .modal-footer { + padding: 1rem 1.5rem 2rem; } @media (max-width: 991.98px) { @@ -146,7 +146,7 @@ border: 1px solid #bee5eb; color: #0c5460; padding: 12px 16px; - margin: 16px; + margin: 6px; border-radius: 4px; font-size: 0.95rem; line-height: 1.5; @@ -170,98 +170,32 @@ box-sizing: border-box; } -/* Hidden sections (display:none handled by JavaScript) */ -.hidden-section { - display: none; +.template-attachments-section { + flex-shrink: 0; + margin-top: 0; + border: 1px solid #dee2e6; + background: #fff; + box-sizing: border-box; + padding: 12px; } -/* Notifications table styling */ -.notification-table { - width: 100%; +#templateAttachmentsLabel { + margin-top: 1rem; } -.notifications-widget .dt-container { - position: relative; - display: flex; - flex-direction: column; - flex: 1 1 auto; - overflow-y: hidden; - overflow-x: hidden; +.template-attachments-section .attachments-table { width: 100%; - min-width: 0; - min-height: 0; + margin-bottom: 0; } -.notifications-widget .dt-scroll-body { - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; -} - -.notifications-widget .dt-scroll { - flex: 1 1 auto; - min-width: 0; - min-height: 0; -} - -.notifications-widget .dt-bootstrap5 > div.dt-layout-table { - flex-grow: 1; - overflow-y: auto; - min-height: 0; -} - - -.dt-unity-footer { - position: fixed; - bottom: 0px; - left: 0px; - width: 100%; - z-index: 99999; - background-color: rgb(255, 255, 255); - box-shadow: rgba(0, 0, 0, 0.1) 0px -2px 10px; - padding: 10px; - height: 60px; - display: flex; - visibility: visible; - opacity: 1; - overflow: visible; +/* Hidden sections (display:none handled by JavaScript) */ +.hidden-section { + display: none; } -.notifications-widget .dt-unity-footer { +/* Notifications table styling */ +.notification-table { width: 100%; - display: flex; - position: fixed; - bottom: 0; - left: 0px; - height: 60px; - z-index: 999999; - background-color: white; - align-items: center; - justify-content: center; -} - -div.dt-container { - padding-bottom: 120px; - overflow: visible; -} - -body { - height: auto; - overflow: visible; - padding-bottom: 100px; -} - -:root { - height: auto; - overflow: visible; -} - -div.dt-scroll-body { - max-height: calc(-80px + 100vh); - height: calc(-80px + 100vh); - overflow: visible; - overflow-y: auto; - position: relative; } /* Notification table action buttons */ @@ -278,13 +212,13 @@ div.dt-scroll-body { .dt-column-title, table.dataTable thead th { color: #fff; font-weight: 500; - font-size: 18px; + font-size: 16px; } table.dataTable td { word-wrap: break-word; max-width: 250px; - font-size: 15px; + font-size: 14px; } .unt-btn-outline-primary { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js index 837d7effe..6a1e912ec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js @@ -553,12 +553,112 @@ const preview = document.getElementById('templatePreview'); if (sel === null || preview === null) return; const val = sel.value; + updateTemplateAttachments(val); fetch('/api/form-notifications/templates').then(r => r.json()).then(list => { const t = list.find(x => String(x.id) === String(val)); renderTemplatePreview(preview, t); }); } + function notifyAttachmentCount(count) { + if (count === 0) return; + + Swal.fire({ + toast: true, + position: 'top-end', + icon: 'info', + text: count === 1 ? '1 attachment is associated with this template.' : `${count} attachments are associated with this template.`, + showConfirmButton: false, + timer: 3000, + timerProgressBar: true + }); + } + + function updateTemplateAttachments(templateId) { + const section = document.getElementById('template-attachments-section'); + const label = document.getElementById('templateAttachmentsLabel'); + const countLabel = document.getElementById('templateAttachmentsCount'); + const table = $('#TemplateAttachmentsTable'); + if (!section || !table.length) return; + + if ($.fn.dataTable.isDataTable(table)) { + table.DataTable().destroy(); + } + section.classList.add('hidden-section'); + label?.classList.add('hidden-section'); + if (countLabel) countLabel.textContent = '0'; + + if (!templateId) { + return; + } + + table.DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: false, + order: [[2, 'asc']], + searching: false, + paging: false, + select: false, + info: false, + scrollX: true, + scrollY: '80px', // ~2 rows visible before scrolling + scrollCollapse: true, + drawCallback: function () { + const count = this.api().rows().count(); + if (countLabel) countLabel.textContent = String(count); + section.classList.toggle('hidden-section', count === 0); + label?.classList.toggle('hidden-section', count === 0); + notifyAttachmentCount(count); + }, + ajax: abp.libs.datatables.createAjax( + unity.notifications.emails.emailLogAttachment.getListByTemplateId, + function () { return templateId; }, + function (result) { return { data: result }; } + ), + columnDefs: [ + { + title: '', + width: '40px', + className: 'text-center', + orderable: false, + render: function () { + return ''; + } + }, + { + title: 'Document Name', + data: 'fileName', + className: 'data-table-header text-break', + width: '55%' + }, + { + title: 'Date', + data: 'time', + className: 'data-table-header', + width: '130px', + render: function (data, type) { + if (type === 'display' || type === 'filter') { + return new Date(data).toDateString(); + } + return data; + } + }, + { + title: 'File Size', + data: 'fileSize', + className: 'data-table-header', + width: '90px', + render: function (data) { + if (data === null || data === undefined) return '—'; + const mb = data * 0.000001; + return mb >= 1 ? mb.toFixed(2) + ' MB' : (data / 1024).toFixed(0) + ' KB'; + } + } + ] + }) + ); + } + function showModal() { resetValidationState(); @@ -575,6 +675,7 @@ document.getElementById('eventOptions')?.classList.add('hidden-section'); document.getElementById('recipientOptions')?.classList.add('hidden-section'); renderTemplatePreview(document.getElementById('templatePreview'), null); + updateTemplateAttachments(''); const modalEl = document.getElementById('notificationModal'); if (modalEl === null) return; @@ -659,9 +760,7 @@ if (modalEl && modalEl.parentElement !== document.body) { document.body.appendChild(modalEl); } - - console.debug('init() starting'); - + formId = document.getElementById('applicationFormId')?.value; if (!formId) { console.warn('formId not found, returning from init()'); @@ -675,7 +774,10 @@ if (modalEl) { // Always reset validation when modal is fully closed - modalEl.addEventListener('hidden.bs.modal', () => resetValidationState()); + modalEl.addEventListener('hidden.bs.modal', () => { + resetValidationState(); + updateTemplateAttachments(''); + }); // Also reset when modal starts opening modalEl.addEventListener('show.bs.modal', () => resetValidationState()); // Refresh select2 when modal is shown @@ -718,6 +820,16 @@ e.target.classList.remove('is-invalid'); updatePreview(); }); + document.getElementById('templateConfigurationLink')?.addEventListener('click', () => { + localStorage.setItem('ConfigurationManagement_ActiveMenu', 'notifications-menu-item'); + localStorage.setItem('notifications-active-tab', 'nav-template-tab'); + const templateId = document.getElementById('templateSelect')?.value; + if (templateId) { + localStorage.setItem('notifications-template-to-select', templateId); + } else { + localStorage.removeItem('notifications-template-to-select'); + } + }); ['dateType', 'moduleSelect', 'statusSelect'].forEach(id => { document.getElementById(id)?.addEventListener('change', (e) => { e.target.classList.remove('is-invalid');