fix: channel save tracking conflict and test modal rendering
- Use AsNoTracking() on all EmailChannelRepository read methods to prevent
EF identity conflict when Update() attaches a new entity with same key
- Move test modal to @section Scripts rendered at end of <body> so
bootstrap.Modal is available and modal is not nested inside card DOM
- Add @RenderSection('Scripts') forwarding in _EditorLayout to bubble
child scripts sections up to _Layout
- Switch Test button to programmatic bootstrap.Modal() open instead of
data-bs-toggle (more reliable across layout section boundaries)
Ref: IT-628
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
|
||||
public async Task<IReadOnlyList<EmailChannel>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
var entities = await DbSet
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.ServiceName)
|
||||
.ThenBy(x => x.Priority)
|
||||
.ToListAsync(ct);
|
||||
@@ -26,6 +27,7 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
|
||||
public async Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
||||
{
|
||||
var entities = await DbSet
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ServiceName == serviceName)
|
||||
.OrderBy(x => x.Priority)
|
||||
.ToListAsync(ct);
|
||||
@@ -35,7 +37,7 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
|
||||
|
||||
public async Task<EmailChannel?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
EmailChannelEntity? entity = await DbSet.FindAsync([id], ct);
|
||||
EmailChannelEntity? entity = await DbSet.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
return entity is null ? null : MapToDomain(entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</button>
|
||||
@if (!Model.IsNew)
|
||||
{
|
||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#testModal">
|
||||
<button type="button" class="btn btn-success" onclick="new bootstrap.Modal(document.getElementById('testModal')).show()">
|
||||
<i class="bi bi-send me-1"></i> Test
|
||||
</button>
|
||||
}
|
||||
@@ -135,62 +135,64 @@
|
||||
|
||||
@if (!Model.IsNew)
|
||||
{
|
||||
<div class="modal fade" id="testModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-send me-2"></i>Send Test Email</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
||||
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
||||
<div id="testResult" class="mt-3" style="display:none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')">
|
||||
<i class="bi bi-send me-1"></i> Send
|
||||
</button>
|
||||
@section Scripts {
|
||||
<div class="modal fade" id="testModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-send me-2"></i>Send Test Email</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
||||
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
||||
<div id="testResult" class="mt-3" style="display:none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')">
|
||||
<i class="bi bi-send me-1"></i> Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function sendTestEmail(channelId) {
|
||||
const toEmail = document.getElementById('testToEmail').value.trim();
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
const sendBtn = document.getElementById('testSendBtn');
|
||||
<script>
|
||||
async function sendTestEmail(channelId) {
|
||||
const toEmail = document.getElementById('testToEmail').value.trim();
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
const sendBtn = document.getElementById('testSendBtn');
|
||||
|
||||
if (!toEmail) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<div class="alert alert-warning mb-0">Please enter a recipient email.</div>';
|
||||
return;
|
||||
if (!toEmail) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<div class="alert alert-warning mb-0">Please enter a recipient email.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/admin/channels/${channelId}/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toEmail })
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = data.success
|
||||
? `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i>${data.message}</div>`
|
||||
: `<div class="alert alert-danger mb-0"><i class="bi bi-x-circle me-1"></i>${data.message}</div>`;
|
||||
} catch (e) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger mb-0">Request failed: ${e.message}</div>`;
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send';
|
||||
}
|
||||
}
|
||||
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/admin/channels/${channelId}/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toEmail })
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = data.success
|
||||
? `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i>${data.message}</div>`
|
||||
: `<div class="alert alert-danger mb-0"><i class="bi bi-x-circle me-1"></i>${data.message}</div>`;
|
||||
} catch (e) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger mb-0">Request failed: ${e.message}</div>`;
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@RenderSection("Scripts", required: false)
|
||||
}
|
||||
|
||||
@@ -51,5 +51,6 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmGBE+rYG8O9HP+CyEb1BQGE8B8Z"
|
||||
crossorigin="anonymous"></script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user