feat(ui): notification detail modal shows message body + recipients
This commit is contained in:
@@ -268,6 +268,64 @@
|
||||
<dd class="col-sm-9 text-danger">@d.LastError</dd>
|
||||
}
|
||||
</dl>
|
||||
|
||||
@* ── Recipients ── *@
|
||||
<hr />
|
||||
<h6 class="mb-2">Recipients</h6>
|
||||
@if (_detailLoading)
|
||||
{
|
||||
<div class="text-muted small">
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
Loading details…
|
||||
</div>
|
||||
}
|
||||
else if (_detailError != null)
|
||||
{
|
||||
<div class="text-danger small">@_detailError</div>
|
||||
}
|
||||
else if (_detail != null)
|
||||
{
|
||||
var recipients = ParseRecipients(_detail.ResolvedTargets);
|
||||
if (recipients.Count > 0)
|
||||
{
|
||||
<ul class="mb-0">
|
||||
@foreach (var recipient in recipients)
|
||||
{
|
||||
<li>@recipient</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-muted small">
|
||||
Not yet resolved — recipients are resolved from list
|
||||
"@d.ListName" at delivery time.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@* ── Body ── *@
|
||||
<hr />
|
||||
<h6 class="mb-2">Message body</h6>
|
||||
@if (_detailLoading)
|
||||
{
|
||||
<div class="text-muted small">
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
Loading details…
|
||||
</div>
|
||||
}
|
||||
else if (_detailError != null)
|
||||
{
|
||||
<div class="text-danger small">@_detailError</div>
|
||||
}
|
||||
else if (_detail != null)
|
||||
{
|
||||
@* Email bodies are plain text (design: BCC delivery, plain text).
|
||||
Rendered as preformatted text — never as a MarkupString, which
|
||||
would be an XSS vector. *@
|
||||
<pre class="border rounded bg-light p-2 mb-0"
|
||||
style="max-height: 320px; overflow: auto; white-space: pre-wrap; word-break: break-word;">@_detail.Body</pre>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (d.Status == "Parked")
|
||||
@@ -304,6 +362,9 @@
|
||||
|
||||
// Row detail modal
|
||||
private NotificationSummary? _detailNotification;
|
||||
private NotificationDetail? _detail;
|
||||
private bool _detailLoading;
|
||||
private string? _detailError;
|
||||
|
||||
// Filters
|
||||
private string _statusFilter = string.Empty;
|
||||
@@ -440,9 +501,80 @@
|
||||
_actionInProgress = false;
|
||||
}
|
||||
|
||||
private void ShowDetail(NotificationSummary n) => _detailNotification = n;
|
||||
private async Task ShowDetail(NotificationSummary n)
|
||||
{
|
||||
// The summary fields render immediately; Body + recipients fill in once the
|
||||
// full-detail fetch completes.
|
||||
_detailNotification = n;
|
||||
_detail = null;
|
||||
_detailError = null;
|
||||
_detailLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
private void CloseDetail() => _detailNotification = null;
|
||||
try
|
||||
{
|
||||
var response = await CommunicationService.GetNotificationDetailAsync(
|
||||
new NotificationDetailRequest(Guid.NewGuid().ToString("N"), n.NotificationId));
|
||||
if (response.Success && response.Detail != null)
|
||||
{
|
||||
_detail = response.Detail;
|
||||
}
|
||||
else
|
||||
{
|
||||
_detailError = response.ErrorMessage ?? "Failed to load notification detail.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_detailError = $"Failed to load notification detail: {ex.Message}";
|
||||
}
|
||||
_detailLoading = false;
|
||||
}
|
||||
|
||||
private void CloseDetail()
|
||||
{
|
||||
_detailNotification = null;
|
||||
_detail = null;
|
||||
_detailError = null;
|
||||
_detailLoading = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort parse of <c>ResolvedTargets</c> into individual recipient addresses.
|
||||
/// The field may be a JSON string array, or a comma/semicolon-separated string.
|
||||
/// Returns an empty list when null/empty.
|
||||
/// </summary>
|
||||
private static List<string> ParseRecipients(string? resolvedTargets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(resolvedTargets))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var trimmed = resolvedTargets.Trim();
|
||||
if (trimmed.StartsWith('['))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = System.Text.Json.JsonSerializer.Deserialize<List<string>>(trimmed);
|
||||
if (parsed != null)
|
||||
{
|
||||
return parsed
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r))
|
||||
.Select(r => r.Trim())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// Not valid JSON — fall through to the delimiter-split path.
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed
|
||||
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task RetryFromDetail(NotificationSummary n)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user