Merge pull request 'chore: update package versions and refactor TransactionBehavior' (#4) from development into main
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
@@ -4,10 +4,10 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Entity Framework Core -->
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<!-- MediatR -->
|
||||
<PackageVersion Include="MediatR" Version="12.4.1" />
|
||||
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- HrynCo shared packages -->
|
||||
<PackageVersion Include="HrynCo.Common" Version="1.0.11" />
|
||||
<PackageVersion Include="HrynCo.RabbitMq" Version="1.0.15" />
|
||||
<PackageVersion Include="HrynCo.DAL.Abstract" Version="1.0.1" />
|
||||
<PackageVersion Include="HrynCo.DAL.Abstract" Version="1.0.10" />
|
||||
<PackageVersion Include="HrynCo.DAL.EF" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
|
||||
@@ -18,11 +18,15 @@ public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TReque
|
||||
|
||||
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) =>
|
||||
_profiler.MeasureExecutionAsync(
|
||||
() => _unitOfWork.ExecuteInTransactionAsync(async () =>
|
||||
async () =>
|
||||
{
|
||||
TResponse response = await next();
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return response;
|
||||
}),
|
||||
TResponse? response = default;
|
||||
await _unitOfWork.ExecuteInTransactionAsync(async () =>
|
||||
{
|
||||
response = await next();
|
||||
});
|
||||
|
||||
return response!;
|
||||
},
|
||||
typeof(TRequest).Name);
|
||||
}
|
||||
@@ -64,6 +64,35 @@
|
||||
<div class="form-text">JSON array of <code>{"name":"...", "required":true|false}</code></div>
|
||||
</div>
|
||||
|
||||
<div class="template-preview-panel mb-3">
|
||||
<div class="template-preview-panel-header">
|
||||
<div>
|
||||
<div class="template-preview-title">Preview</div>
|
||||
<div class="template-preview-subtitle">Rendered with sample values from the variable list.</div>
|
||||
</div>
|
||||
<span id="previewStatus" class="badge text-bg-secondary">Ready</span>
|
||||
</div>
|
||||
|
||||
<div class="template-preview-grid">
|
||||
<div class="template-preview-source">
|
||||
<div class="template-preview-section-title">Sample values</div>
|
||||
<div id="previewVariables" class="template-preview-variables"></div>
|
||||
<div class="form-text mt-2">Change these values to see the rendered output update immediately.</div>
|
||||
</div>
|
||||
|
||||
<div class="template-preview-output">
|
||||
<div class="template-preview-section-title">Rendered subject</div>
|
||||
<div id="previewSubject" class="template-preview-subject"></div>
|
||||
|
||||
<div class="template-preview-section-title mt-3">Rendered HTML</div>
|
||||
<iframe id="previewHtmlFrame" class="template-preview-frame" title="Email HTML preview"></iframe>
|
||||
|
||||
<div class="template-preview-section-title mt-3">Rendered text</div>
|
||||
<pre id="previewText" class="template-preview-text mb-0"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section FormActions {
|
||||
<button type="submit" form="templateForm" class="btn btn-primary">
|
||||
<i class="bi bi-floppy me-1"></i> Save
|
||||
@@ -72,4 +101,175 @@
|
||||
<i class="bi bi-x-lg me-1"></i> Cancel
|
||||
</a>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(function () {
|
||||
const subjectField = document.getElementById('Subject');
|
||||
const htmlField = document.getElementById('HtmlBody');
|
||||
const textField = document.getElementById('TextBody');
|
||||
const variablesField = document.getElementById('VariablesJson');
|
||||
const previewVariablesHost = document.getElementById('previewVariables');
|
||||
const previewSubject = document.getElementById('previewSubject');
|
||||
const previewText = document.getElementById('previewText');
|
||||
const previewFrame = document.getElementById('previewHtmlFrame');
|
||||
const previewStatus = document.getElementById('previewStatus');
|
||||
|
||||
if (!subjectField || !htmlField || !textField || !variablesField || !previewVariablesHost || !previewSubject || !previewText || !previewFrame || !previewStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function parseVariables() {
|
||||
const raw = variablesField.value?.trim() || '[]';
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Variables JSON must be an array.');
|
||||
}
|
||||
|
||||
return parsed
|
||||
.filter(item => item && typeof item.Name === 'string' && item.Name.trim().length > 0)
|
||||
.map(item => ({
|
||||
name: item.Name.trim(),
|
||||
required: !!item.Required
|
||||
}));
|
||||
}
|
||||
|
||||
function buildSampleValue(name) {
|
||||
return `Sample ${name}`;
|
||||
}
|
||||
|
||||
function renderVariableInputs() {
|
||||
let variables = [];
|
||||
|
||||
try {
|
||||
variables = parseVariables();
|
||||
previewStatus.className = 'badge text-bg-secondary';
|
||||
previewStatus.textContent = 'Ready';
|
||||
} catch (error) {
|
||||
previewVariablesHost.innerHTML = `<div class="alert alert-warning mb-0">${escapeHtml(error.message || 'Invalid variables JSON')}</div>`;
|
||||
previewStatus.className = 'badge text-bg-danger';
|
||||
previewStatus.textContent = 'Invalid JSON';
|
||||
updatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (variables.length === 0) {
|
||||
previewVariablesHost.innerHTML = '<div class="text-muted small">No variables defined.</div>';
|
||||
updatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValues = readVariableValues();
|
||||
previewVariablesHost.innerHTML = variables.map(variable => {
|
||||
const value = Object.prototype.hasOwnProperty.call(currentValues, variable.name)
|
||||
? currentValues[variable.name]
|
||||
: buildSampleValue(variable.name);
|
||||
return `
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold mb-1">${escapeHtml(variable.name)}${variable.required ? ' *' : ''}</label>
|
||||
<input type="text" class="form-control form-control-sm preview-variable-input" data-variable-name="${escapeHtml(variable.name)}" value="${escapeHtml(value)}" />
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
previewVariablesHost.querySelectorAll('.preview-variable-input').forEach(input => {
|
||||
input.addEventListener('input', updatePreview);
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function readVariableValues() {
|
||||
const values = {};
|
||||
previewVariablesHost.querySelectorAll('.preview-variable-input').forEach(input => {
|
||||
values[input.dataset.variableName] = input.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function interpolate(text, values) {
|
||||
let result = text || '';
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
const token = new RegExp(`\\{\\{${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`, 'g');
|
||||
result = result.replace(token, values[key] ?? '');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
let values = {};
|
||||
|
||||
try {
|
||||
values = readVariableValues();
|
||||
} catch (error) {
|
||||
values = {};
|
||||
}
|
||||
|
||||
const renderedSubject = interpolate(subjectField.value, values);
|
||||
const renderedHtml = interpolate(htmlField.value, values);
|
||||
const renderedText = interpolate(textField.value, values);
|
||||
|
||||
previewSubject.textContent = renderedSubject || '(empty subject)';
|
||||
previewText.textContent = renderedText || '(empty text body)';
|
||||
previewFrame.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
background: #eef2f7;
|
||||
color: #1f2937;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.email-shell {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, .08);
|
||||
}
|
||||
.email-body {
|
||||
padding: 28px 36px;
|
||||
}
|
||||
img { max-width: 100%; height: auto; }
|
||||
a { color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-shell">
|
||||
<div class="email-body">
|
||||
${renderedHtml || '<div style="color:#6b7280">No HTML body provided.</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
variablesField.addEventListener('input', renderVariableInputs);
|
||||
subjectField.addEventListener('input', updatePreview);
|
||||
htmlField.addEventListener('input', updatePreview);
|
||||
textField.addEventListener('input', updatePreview);
|
||||
|
||||
renderVariableInputs();
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
||||
@@ -154,6 +154,109 @@ body {
|
||||
border-radius: 0 0 .5rem .5rem !important;
|
||||
}
|
||||
|
||||
/* ── Email template preview ───────────────────────────── */
|
||||
.template-preview-panel {
|
||||
border: 1px solid #dce3eb;
|
||||
border-radius: .75rem;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, .06);
|
||||
}
|
||||
|
||||
.template-preview-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: linear-gradient(180deg, #f9fbff 0%, #f3f6fb 100%);
|
||||
border-bottom: 1px solid #e1e7ef;
|
||||
}
|
||||
|
||||
.template-preview-title {
|
||||
font-size: .9rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.template-preview-subtitle {
|
||||
font-size: .82rem;
|
||||
color: #6b7280;
|
||||
margin-top: .15rem;
|
||||
}
|
||||
|
||||
.template-preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
.template-preview-source,
|
||||
.template-preview-output {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.template-preview-section-title {
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.template-preview-variables {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.template-preview-variables .form-control-sm {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.template-preview-subject {
|
||||
padding: .75rem 1rem;
|
||||
border: 1px dashed #cfd8e3;
|
||||
border-radius: .5rem;
|
||||
background: #f8fafc;
|
||||
min-height: 3rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.template-preview-frame {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: .5rem;
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.template-preview-text {
|
||||
padding: .75rem 1rem;
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: .5rem;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
min-height: 120px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.template-preview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.template-preview-frame {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Form-section divider ─────────────────────────────── */
|
||||
.form-section-title {
|
||||
font-size: .68rem;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
DB_NAME=notification_service
|
||||
DB_USER=postgres
|
||||
DB_PASS=postgres
|
||||
VOLUME_PREFIX=ns-dev
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
RABBITMQ_AMQP_PORT=5672
|
||||
RABBITMQ_MANAGEMENT_PORT=15672
|
||||
WEB_PORT=5200
|
||||
Reference in New Issue
Block a user