diff --git a/HrynCo.NotificationService.Web.IntegrationTests/TemplateCatalogPresentationTests.cs b/HrynCo.NotificationService.Web.IntegrationTests/TemplateCatalogPresentationTests.cs index 253829b..85c945f 100644 --- a/HrynCo.NotificationService.Web.IntegrationTests/TemplateCatalogPresentationTests.cs +++ b/HrynCo.NotificationService.Web.IntegrationTests/TemplateCatalogPresentationTests.cs @@ -8,6 +8,37 @@ namespace HrynCo.NotificationService.Web.IntegrationTests; public sealed class TemplateCatalogPresentationTests(TemplateDatabaseFixture fixture) : IClassFixture { + [Fact] + public async Task EditorTabsShowSavedVersionsAfterInvalidUnsavedChanges() + { + var connection = await fixture.CreateDatabaseAsync(); + await using var app = new TemplateWebApplicationFactory(connection); + using var scope = app.Services.CreateScope(); + var service = (await scope.ServiceProvider.GetRequiredService().GetAllAsync()).Single(); + var versions = scope.ServiceProvider.GetRequiredService(); + await versions.AddAsync(new EmailTemplate + { + ServiceName = service.Name, Key = "TabStatus", LanguageCode = "en", + Subject = "Saved", HtmlBody = "Saved", TextBody = "Saved" + }); + var definition = (await scope.ServiceProvider.GetRequiredService() + .GetDefinitionAsync(service.Name, "TabStatus"))!; + using var client = app.CreateClient(new() { AllowAutoRedirect = false }); + var baseUrl = $"/admin/services/{service.Id}/templates"; + var html = await client.GetStringAsync($"{baseUrl}/{definition.Id}"); + Assert.Matches("(?s)en\\s*Present", html); + Assert.Matches("(?s)uk\\s*Missing", html); + + var fields = TemplateEditorTestFields.Read(html); + TemplateEditorTestFields.SetLanguage(fields, "uk", "Unsaved", "", ""); + fields["Languages[0].IsPresent"] = "false"; + var response = await client.PostAsync(baseUrl + "/save", new FormUrlEncodedContent(fields)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var invalidHtml = await response.Content.ReadAsStringAsync(); + Assert.Matches("(?s)en\\s*Present", invalidHtml); + Assert.Matches("(?s)uk\\s*Missing", invalidHtml); + } + [Fact] public async Task CatalogStatusesUseExactCodes_AndEmptyDefinitionsShowEveryLanguageMissing() { diff --git a/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs b/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs index a5284d3..441a735 100644 --- a/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs +++ b/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs @@ -69,8 +69,12 @@ public sealed class AdminTemplatesController(IMediator mediator, IEmailTemplateD var service = await services.GetAsync(serviceId, ct); if (service is null || model.ServiceId != serviceId || model.ServiceName != service.Name) return NotFound(); - if (model.Id is Guid definitionId && await OwnedDefinitionAsync(service, definitionId, ct) is null) + var persisted = model.Id is Guid definitionId ? await OwnedDefinitionAsync(service, definitionId, ct) : null; + if (model.Id is not null && persisted is null) return NotFound(); + var presentCodes = persisted?.Languages.Select(l => l.LanguageCode).ToHashSet(StringComparer.Ordinal); + foreach (var language in model.Languages) + language.IsPresent = presentCodes?.Contains(language.LanguageCode) == true; ValidateLanguageCatalog(model, service); if (!ModelState.IsValid) return EditorView(model, service); @@ -226,7 +230,8 @@ public sealed class AdminTemplatesController(IMediator mediator, IEmailTemplateD VariablesJson = JsonSerializer.Serialize(definition.Variables, JsonOptions), Languages = definition.Languages.Select(l => new EmailTemplateLanguageEditViewModel { - LanguageCode = l.LanguageCode, Subject = l.Subject, HtmlBody = l.HtmlBody, TextBody = l.TextBody + LanguageCode = l.LanguageCode, Subject = l.Subject, HtmlBody = l.HtmlBody, TextBody = l.TextBody, + IsPresent = true }).ToList() }; } diff --git a/HrynCo.NotificationService.Web/Controllers/Admin/ViewModels/EmailTemplateLanguageEditViewModel.cs b/HrynCo.NotificationService.Web/Controllers/Admin/ViewModels/EmailTemplateLanguageEditViewModel.cs index 23a84e6..341f6fc 100644 --- a/HrynCo.NotificationService.Web/Controllers/Admin/ViewModels/EmailTemplateLanguageEditViewModel.cs +++ b/HrynCo.NotificationService.Web/Controllers/Admin/ViewModels/EmailTemplateLanguageEditViewModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace HrynCo.NotificationService.Web.Controllers.Admin.ViewModels; @@ -12,6 +13,8 @@ public sealed class EmailTemplateLanguageEditViewModel : IValidatableObject public string HtmlBody { get; set; } = ""; [DisplayFormat(ConvertEmptyStringToNull = false)] public string TextBody { get; set; } = ""; + [BindNever] + public bool IsPresent { get; set; } public bool HasContent => !string.IsNullOrWhiteSpace(Subject) || !string.IsNullOrWhiteSpace(HtmlBody) || !string.IsNullOrWhiteSpace(TextBody); public IEnumerable Validate(ValidationContext validationContext) diff --git a/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml b/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml index ee81b43..ac91991 100644 --- a/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml +++ b/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml @@ -34,18 +34,22 @@
- +
One contract for every language: [{"name":"UserName","required":true}]
-

Every configured service language has a tab. Leave all three content fields blank for an unused language; clear them to remove an existing version. Shared variables and the template remain.

+

Every configured service language has a tab. Present and Missing show saved versions. Leave all three content fields blank for an unused language; clear them to remove an existing version. Shared variables and the template remain.

@if (Model.Languages.Count == 0) {

No service languages configured. You can save the key and shared variables now, then add language codes in Edit Service.

}