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)
|
public async Task<IReadOnlyList<EmailChannel>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var entities = await DbSet
|
var entities = await DbSet
|
||||||
|
.AsNoTracking()
|
||||||
.OrderBy(x => x.ServiceName)
|
.OrderBy(x => x.ServiceName)
|
||||||
.ThenBy(x => x.Priority)
|
.ThenBy(x => x.Priority)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
@@ -26,6 +27,7 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
|
|||||||
public async Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
public async Task<IReadOnlyList<EmailChannel>> GetByServiceAsync(string serviceName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var entities = await DbSet
|
var entities = await DbSet
|
||||||
|
.AsNoTracking()
|
||||||
.Where(x => x.ServiceName == serviceName)
|
.Where(x => x.ServiceName == serviceName)
|
||||||
.OrderBy(x => x.Priority)
|
.OrderBy(x => x.Priority)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
@@ -35,7 +37,7 @@ internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>,
|
|||||||
|
|
||||||
public async Task<EmailChannel?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
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);
|
return entity is null ? null : MapToDomain(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</button>
|
</button>
|
||||||
@if (!Model.IsNew)
|
@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
|
<i class="bi bi-send me-1"></i> Test
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -135,62 +135,64 @@
|
|||||||
|
|
||||||
@if (!Model.IsNew)
|
@if (!Model.IsNew)
|
||||||
{
|
{
|
||||||
<div class="modal fade" id="testModal" tabindex="-1">
|
@section Scripts {
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal fade" id="testModal" tabindex="-1">
|
||||||
<div class="modal-content">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-header">
|
<div class="modal-content">
|
||||||
<h5 class="modal-title"><i class="bi bi-send me-2"></i>Send Test Email</h5>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<h5 class="modal-title"><i class="bi bi-send me-2"></i>Send Test Email</h5>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
<div class="modal-body">
|
</div>
|
||||||
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
<div class="modal-body">
|
||||||
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
|
||||||
<div id="testResult" class="mt-3" style="display:none"></div>
|
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
|
||||||
</div>
|
<div id="testResult" class="mt-3" style="display:none"></div>
|
||||||
<div class="modal-footer">
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')">
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
<i class="bi bi-send me-1"></i> Send
|
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')">
|
||||||
</button>
|
<i class="bi bi-send me-1"></i> Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function sendTestEmail(channelId) {
|
async function sendTestEmail(channelId) {
|
||||||
const toEmail = document.getElementById('testToEmail').value.trim();
|
const toEmail = document.getElementById('testToEmail').value.trim();
|
||||||
const resultDiv = document.getElementById('testResult');
|
const resultDiv = document.getElementById('testResult');
|
||||||
const sendBtn = document.getElementById('testSendBtn');
|
const sendBtn = document.getElementById('testSendBtn');
|
||||||
|
|
||||||
if (!toEmail) {
|
if (!toEmail) {
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
resultDiv.innerHTML = '<div class="alert alert-warning mb-0">Please enter a recipient email.</div>';
|
resultDiv.innerHTML = '<div class="alert alert-warning mb-0">Please enter a recipient email.</div>';
|
||||||
return;
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmGBE+rYG8O9HP+CyEb1BQGE8B8Z"
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmGBE+rYG8O9HP+CyEb1BQGE8B8Z"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
@RenderSection("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user