Compare commits
3 Commits
637af06a9c
...
7b77062aa7
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b77062aa7 | |||
| 9490718c04 | |||
| 0d1aa4f6be |
@@ -4,10 +4,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Entity Framework Core -->
|
<!-- Entity Framework Core -->
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
<!-- MediatR -->
|
<!-- MediatR -->
|
||||||
<PackageVersion Include="MediatR" Version="12.4.1" />
|
<PackageVersion Include="MediatR" Version="12.4.1" />
|
||||||
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<!-- HrynCo shared packages -->
|
<!-- HrynCo shared packages -->
|
||||||
<PackageVersion Include="HrynCo.Common" Version="1.0.11" />
|
<PackageVersion Include="HrynCo.Common" Version="1.0.11" />
|
||||||
<PackageVersion Include="HrynCo.RabbitMq" Version="1.0.15" />
|
<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="HrynCo.DAL.EF" Version="1.0.1" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
<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) =>
|
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) =>
|
||||||
_profiler.MeasureExecutionAsync(
|
_profiler.MeasureExecutionAsync(
|
||||||
() => _unitOfWork.ExecuteInTransactionAsync(async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
TResponse response = await next();
|
TResponse? response = default;
|
||||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
await _unitOfWork.ExecuteInTransactionAsync(async () =>
|
||||||
return response;
|
{
|
||||||
}),
|
response = await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return response!;
|
||||||
|
},
|
||||||
typeof(TRequest).Name);
|
typeof(TRequest).Name);
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,35 @@
|
|||||||
<div class="form-text">JSON array of <code>{"name":"...", "required":true|false}</code></div>
|
<div class="form-text">JSON array of <code>{"name":"...", "required":true|false}</code></div>
|
||||||
</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 {
|
@section FormActions {
|
||||||
<button type="submit" form="templateForm" class="btn btn-primary">
|
<button type="submit" form="templateForm" class="btn btn-primary">
|
||||||
<i class="bi bi-floppy me-1"></i> Save
|
<i class="bi bi-floppy me-1"></i> Save
|
||||||
@@ -72,4 +101,175 @@
|
|||||||
<i class="bi bi-x-lg me-1"></i> Cancel
|
<i class="bi bi-x-lg me-1"></i> Cancel
|
||||||
</a>
|
</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>
|
</form>
|
||||||
|
|||||||
@@ -14,3 +14,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@RenderSection("Scripts", required: false)
|
||||||
|
|||||||
@@ -154,6 +154,109 @@ body {
|
|||||||
border-radius: 0 0 .5rem .5rem !important;
|
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 divider ─────────────────────────────── */
|
||||||
.form-section-title {
|
.form-section-title {
|
||||||
font-size: .68rem;
|
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