From fa58d91cc72a82dc482a0c173bfb9fca31c1aecf Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:24:37 -0600 Subject: [PATCH] replace template tree view with template table --- CHANGELOG.md | 2 + .../Scheduling/Commands/CopyTemplate.cs | 6 + .../Commands/CopyTemplateHandler.cs | 100 ++++++++ .../Scheduling/Queries/GetAllTemplates.cs | 3 + .../Queries/GetAllTemplatesHandler.cs | 54 ++++ .../Scheduling/Queries/GetTemplateTree.cs | 5 - .../Queries/GetTemplateTreeHandler.cs | 24 -- ErsatzTV/Pages/Templates.razor | 235 ++++++++++++------ ErsatzTV/Shared/CopyTemplateDialog.razor | 100 ++++++++ 9 files changed, 428 insertions(+), 101 deletions(-) create mode 100644 ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs delete mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs delete mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs create mode 100644 ErsatzTV/Shared/CopyTemplateDialog.razor diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e1055db..61ae5aca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add channel troubleshooting button to channels list - This will open the playback troubleshooting tool in "channel" mode - This mode requires entering a date and time, and will play up to 30 seconds of *one item from that channel's playout* starting at the entered date and time +- Block schedules: add copy template button to templates table ### Fixed - Fix HLS Direct playback with Jellyfin 10.11 @@ -90,6 +91,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Block groups in block list - Template groups in template list - Block groups and blocks in template editor + - Replace template tree view with searchable table (like blocks) ## [25.8.0] - 2025-10-26 ### Added diff --git a/ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs b/ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs new file mode 100644 index 000000000..9b971ba56 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs @@ -0,0 +1,6 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record CopyTemplate(int TemplateId, int NewTemplateGroupId, string NewTemplateName) + : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs new file mode 100644 index 000000000..645266e0a --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs @@ -0,0 +1,100 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.Scheduling.Mapper; + +namespace ErsatzTV.Application.Scheduling; + +public class CopyTemplateHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + CopyTemplate request, + CancellationToken cancellationToken) + { + try + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request, cancellationToken); + return await validation.Apply(p => PerformCopy(dbContext, p, request, cancellationToken)); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return BaseError.New(ex.Message); + } + } + + private static async Task PerformCopy( + TvContext dbContext, + Template template, + CopyTemplate request, + CancellationToken cancellationToken) + { + DetachEntity(dbContext, template); + template.Name = request.NewTemplateName; + template.TemplateGroup = null; + template.TemplateGroupId = request.NewTemplateGroupId; + + foreach (TemplateItem item in template.Items) + { + DetachEntity(dbContext, item); + item.TemplateId = 0; + item.Template = template; + } + + await dbContext.Templates.AddAsync(template, cancellationToken); + await dbContext.TemplateItems.AddRangeAsync(template.Items, cancellationToken); + + await dbContext.SaveChangesAsync(cancellationToken); + + await dbContext.Entry(template).Reference(b => b.TemplateGroup).LoadAsync(cancellationToken); + + return ProjectToViewModel(template); + } + + private static async Task> Validate( + TvContext dbContext, + CopyTemplate request, + CancellationToken cancellationToken) => + (await TemplateMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request)) + .Apply((template, _) => template); + + private static Task> TemplateMustExist( + TvContext dbContext, + CopyTemplate request, + CancellationToken cancellationToken) => + dbContext.Templates + .AsNoTracking() + .Include(t => t.Items) + .SelectOneAsync(p => p.Id, p => p.Id == request.TemplateId, cancellationToken) + .Map(o => o.ToValidation("Template does not exist.")); + + private static async Task> ValidateName(TvContext dbContext, CopyTemplate request) + { + List allNames = await dbContext.Templates + .Where(b => b.TemplateGroupId == request.NewTemplateGroupId) + .Map(ps => ps.Name) + .ToListAsync(); + + Validation result1 = request.NotEmpty(c => c.NewTemplateName) + .Bind(_ => request.NotLongerThan(50)(c => c.NewTemplateName)); + + var result2 = Optional(request.NewTemplateName) + .Where(name => !allNames.Contains(name)) + .ToValidation("Template name must be unique within the template group."); + + return (result1, result2).Apply((_, _) => request.NewTemplateName); + } + + private static void DetachEntity(TvContext db, T entity) where T : class + { + db.Entry(entity).State = EntityState.Detached; + if (entity.GetType().GetProperty("Id") is not null) + { + entity.GetType().GetProperty("Id")!.SetValue(entity, 0); + } + } +} diff --git a/ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs b/ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs new file mode 100644 index 000000000..695cb40cd --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record GetAllTemplates : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs new file mode 100644 index 000000000..f7220761f --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs @@ -0,0 +1,54 @@ +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetAllTemplatesHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(GetAllTemplates request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List