feat(templates): add service language template management
Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage. Ref: IT-1601
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HrynCo.NotificationService.Web.Controllers.Admin;
|
||||
|
||||
[Route("admin/services")]
|
||||
public sealed class AdminServicesController(INotificationServiceRepository services, ILogger<AdminServicesController> logger) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Index(CancellationToken ct) => View(await services.GetAllAsync(ct));
|
||||
|
||||
[HttpGet("create")]
|
||||
public IActionResult Create() => View("Edit", new NotificationServiceEditViewModel());
|
||||
|
||||
[HttpGet("{id:guid}/edit")]
|
||||
public async Task<IActionResult> Edit([FromRoute] Guid id, CancellationToken ct)
|
||||
{
|
||||
var service = await services.GetAsync(id, ct);
|
||||
return service is null ? NotFound() : View(ToViewModel(service));
|
||||
}
|
||||
|
||||
[HttpPost("save")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Save(NotificationServiceEditViewModel model, CancellationToken ct)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return await ServiceEditorViewAsync(model, ct);
|
||||
try
|
||||
{
|
||||
var service = new NotificationServiceInfo
|
||||
{
|
||||
Id = model.Id ?? Guid.Empty, Name = model.Name, Revision = model.Revision, Languages = model.Languages
|
||||
};
|
||||
await services.SaveAsync(service, model.IsNew, ct);
|
||||
return RedirectToAction(nameof(AdminTemplatesController.Index), "AdminTemplates", new { serviceId = service.Id });
|
||||
}
|
||||
catch (EmailTemplateWriteException error)
|
||||
{
|
||||
ModelState.AddModelError("", error.Message);
|
||||
}
|
||||
catch (Exception error) when (error is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(error, "Failed to save notification service.");
|
||||
ModelState.AddModelError("", "Could not save the service. Your entered values are retained; try again.");
|
||||
}
|
||||
return await ServiceEditorViewAsync(model, ct);
|
||||
}
|
||||
|
||||
[HttpGet("{serviceId:guid}/languages/{languageId:guid}/delete")]
|
||||
public async Task<IActionResult> ConfirmLanguageRemoval([FromRoute] Guid serviceId, [FromRoute] Guid languageId, CancellationToken ct)
|
||||
{
|
||||
var model = await RemovalModelAsync(serviceId, languageId, ct);
|
||||
return model is null ? NotFound() : View("DeleteLanguage", model);
|
||||
}
|
||||
|
||||
[HttpPost("{serviceId:guid}/languages/{languageId:guid}/delete")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RemoveLanguage([FromRoute] Guid serviceId, [FromRoute] Guid languageId, ServiceLanguageDeleteViewModel model, CancellationToken ct)
|
||||
{
|
||||
if (!ModelState.IsValid || !model.ConfirmDelete)
|
||||
return await RemovalErrorAsync(serviceId, languageId, "Confirm deletion of the language and all listed versions to continue.", ct);
|
||||
try
|
||||
{
|
||||
await services.RemoveLanguageAsync(serviceId, languageId, model.VersionCount, model.Revision, ct);
|
||||
return RedirectToAction(nameof(Edit), new { id = serviceId });
|
||||
}
|
||||
catch (EmailTemplateWriteException error)
|
||||
{
|
||||
return await RemovalErrorAsync(serviceId, languageId, error.Message, ct);
|
||||
}
|
||||
catch (Exception error) when (error is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(error, "Failed to remove service language.");
|
||||
return await RemovalErrorAsync(serviceId, languageId, "Could not delete the language. No changes were saved; review the count and try again.", ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ServiceEditorViewAsync(NotificationServiceEditViewModel model, CancellationToken ct)
|
||||
{
|
||||
if (model.Id is not Guid id)
|
||||
return View("Edit", model);
|
||||
try
|
||||
{
|
||||
var persisted = await services.GetAsync(id, ct);
|
||||
var counts = persisted?.Languages.ToDictionary(l => l.Id, l => l.VersionCount) ?? [];
|
||||
foreach (var language in model.Languages)
|
||||
language.VersionCount = counts.GetValueOrDefault(language.Id);
|
||||
}
|
||||
catch (Exception error) when (error is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(error, "Failed to refresh service language version counts.");
|
||||
ViewData["LanguageCountsUnavailable"] = true;
|
||||
ModelState.AddModelError("", "Could not refresh language usage counts. Removal is disabled until the service is available.");
|
||||
}
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RemovalErrorAsync(Guid serviceId, Guid languageId, string error, CancellationToken ct)
|
||||
{
|
||||
var model = await RemovalModelAsync(serviceId, languageId, ct);
|
||||
if (model is null)
|
||||
return NotFound();
|
||||
ModelState.Clear();
|
||||
ModelState.AddModelError("", error);
|
||||
return View("DeleteLanguage", model);
|
||||
}
|
||||
|
||||
private async Task<ServiceLanguageDeleteViewModel?> RemovalModelAsync(Guid serviceId, Guid languageId, CancellationToken ct)
|
||||
{
|
||||
var service = await services.GetAsync(serviceId, ct);
|
||||
var language = service?.Languages.SingleOrDefault(l => l.Id == languageId);
|
||||
return language is null ? null : new ServiceLanguageDeleteViewModel
|
||||
{
|
||||
ServiceId = serviceId, LanguageId = languageId, ServiceName = service!.Name, Revision = service.Revision,
|
||||
Code = language.Code, VersionCount = language.VersionCount
|
||||
};
|
||||
}
|
||||
|
||||
private static NotificationServiceEditViewModel ToViewModel(NotificationServiceInfo service) => new()
|
||||
{
|
||||
Id = service.Id, Name = service.Name, Revision = service.Revision, Languages = service.Languages.ToList()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user