Browse Source

block ui improvements (#2646)

* template editor improvements

* more keyboard navigation

* replace template tree view with template table
pull/2647/head
Jason Dove 2 months ago committed by GitHub
parent
commit
21f4439aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs
  3. 100
      ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs
  4. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs
  5. 54
      ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs
  6. 5
      ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs
  7. 24
      ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs
  8. 4
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs
  9. 10
      ErsatzTV/Pages/Blocks.razor
  10. 35
      ErsatzTV/Pages/TemplateEditor.razor
  11. 245
      ErsatzTV/Pages/Templates.razor
  12. 100
      ErsatzTV/Shared/CopyTemplateDialog.razor

8
CHANGELOG.md

@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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
@ -84,6 +85,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -84,6 +85,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use smaller batch size for search index updates (100, down from 1000)
- This should help newly scanned items appear in the UI more quickly
- Replace favicon and logo in background image used for error streams
- Block schedules:
- Auto scroll day view to block item time when adding and removing block items from template
- Allow keyboard selection of
- 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

6
ErsatzTV.Application/Scheduling/Commands/CopyTemplate.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record CopyTemplate(int TemplateId, int NewTemplateGroupId, string NewTemplateName)
: IRequest<Either<BaseError, TemplateViewModel>>;

100
ErsatzTV.Application/Scheduling/Commands/CopyTemplateHandler.cs

@ -0,0 +1,100 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<CopyTemplate, Either<BaseError, TemplateViewModel>>
{
public async Task<Either<BaseError, TemplateViewModel>> Handle(
CopyTemplate request,
CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Template> 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<TemplateViewModel> 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<Validation<BaseError, Template>> Validate(
TvContext dbContext,
CopyTemplate request,
CancellationToken cancellationToken) =>
(await TemplateMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request))
.Apply((template, _) => template);
private static Task<Validation<BaseError, Template>> 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<BaseError>("Template does not exist."));
private static async Task<Validation<BaseError, string>> ValidateName(TvContext dbContext, CopyTemplate request)
{
List<string> allNames = await dbContext.Templates
.Where(b => b.TemplateGroupId == request.NewTemplateGroupId)
.Map(ps => ps.Name)
.ToListAsync();
Validation<BaseError, string> result1 = request.NotEmpty(c => c.NewTemplateName)
.Bind(_ => request.NotLongerThan(50)(c => c.NewTemplateName));
var result2 = Optional(request.NewTemplateName)
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("Template name must be unique within the template group.");
return (result1, result2).Apply((_, _) => request.NewTemplateName);
}
private static void DetachEntity<T>(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);
}
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetAllTemplates.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetAllTemplates : IRequest<List<TemplateViewModel>>;

54
ErsatzTV.Application/Scheduling/Queries/GetAllTemplatesHandler.cs

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetAllTemplatesHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllTemplates, List<TemplateViewModel>>
{
public async Task<List<TemplateViewModel>> Handle(GetAllTemplates request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Template> templates = await dbContext.Templates
.AsNoTracking()
.ToListAsync(cancellationToken);
List<TemplateGroup> templateGroups = await dbContext.TemplateGroups
.AsNoTracking()
.ToListAsync(cancellationToken);
var unusedTemplateGroups = templateGroups.ToList();
// match templates to template groups
foreach (var template in templates)
{
var maybeTemplateGroup = templateGroups.FirstOrDefault(bg => bg.Id == template.TemplateGroupId);
if (maybeTemplateGroup != null)
{
unusedTemplateGroups.Remove(maybeTemplateGroup);
template.TemplateGroup = maybeTemplateGroup;
}
}
// create dummy templates for any groups that have no templates yet
foreach (var unusedGroup in unusedTemplateGroups)
{
var dummyTemplate = new Template
{
Id = unusedGroup.Id * -1,
TemplateGroupId = unusedGroup.Id,
TemplateGroup = unusedGroup,
Name = "(none)"
};
templates.Add(dummyTemplate);
}
return templates.Map(Mapper.ProjectToViewModel)
.OrderBy(b => b.GroupName)
.ThenBy(b => b.Name)
.ToList();
}
}

5
ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
using ErsatzTV.Application.Tree;
namespace ErsatzTV.Application.Scheduling;
public record GetTemplateTree : IRequest<TreeViewModel>;

24
ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs

@ -1,24 +0,0 @@ @@ -1,24 +0,0 @@
using ErsatzTV.Application.Tree;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetTemplateTreeHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetTemplateTree, TreeViewModel>
{
public async Task<TreeViewModel> Handle(
GetTemplateTree request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<TemplateGroup> templateGroups = await dbContext.TemplateGroups
.AsNoTracking()
.Include(g => g.Templates)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
}
}

4
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs

@ -100,7 +100,6 @@ public class BlockPlayoutFillerBuilder( @@ -100,7 +100,6 @@ public class BlockPlayoutFillerBuilder(
collectionEnumerators,
allItems,
filteredExistingHistory,
result.RemoveBefore,
cancellationToken);
return result;
@ -130,7 +129,7 @@ public class BlockPlayoutFillerBuilder( @@ -130,7 +129,7 @@ public class BlockPlayoutFillerBuilder(
// guide group is template item id
// they are reused over multiple days, so we only want to group consecutive items
IEnumerable<IGrouping<int, PlayoutItem>> consecutiveBlocks = allItems
.Where(i => i.FinishOffset > result.RemoveBefore.IfNone(SystemTime.MinValueUtc))
.Where(i => i.FinishOffset > removeBefore.IfNone(SystemTime.MinValueUtc))
.GroupConsecutiveBy(item => item.GuideGroup);
foreach (IGrouping<int, PlayoutItem> blockGroup in consecutiveBlocks)
{
@ -303,7 +302,6 @@ public class BlockPlayoutFillerBuilder( @@ -303,7 +302,6 @@ public class BlockPlayoutFillerBuilder(
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
List<PlayoutItem> allItems,
List<PlayoutHistory> filteredExistingHistory,
Option<DateTimeOffset> removeBefore,
CancellationToken cancellationToken)
{
// find all unscheduled periods

10
ErsatzTV/Pages/Blocks.razor

@ -30,10 +30,10 @@ @@ -30,10 +30,10 @@
<div class="d-flex">
<MudText>Block Group</MudText>
</div>
<MudSelect @bind-Value="_selectedBlockGroup">
<MudSelect T="string" ValueChanged="@(name => UpdateSelectedBlockGroup(name))">
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
{
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
<MudSelectItem Value="@blockGroup.Name">@blockGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
@ -192,6 +192,12 @@ @@ -192,6 +192,12 @@
}
}
private void UpdateSelectedBlockGroup(string blockGroupName)
{
_selectedBlockGroup = _blockGroups.Find(bg => bg.Name == blockGroupName);
InvokeAsync(StateHasChanged);
}
private async Task AddBlock()
{
if (_selectedBlockGroup is not null && !string.IsNullOrWhiteSpace(_blockName))

35
ErsatzTV/Pages/TemplateEditor.razor

@ -35,10 +35,10 @@ @@ -35,10 +35,10 @@
<div class="d-flex">
<MudText>Block Group</MudText>
</div>
<MudSelect T="BlockGroupViewModel" ValueChanged="@(vm => UpdateBlockGroupItems(vm))">
<MudSelect T="string" ValueChanged="@(name => UpdateBlockGroupItems(name))">
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
{
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
<MudSelectItem Value="@blockGroup.Name">@blockGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
@ -46,10 +46,10 @@ @@ -46,10 +46,10 @@
<div class="d-flex">
<MudText>Block</MudText>
</div>
<MudSelect T="BlockViewModel" @bind-value="_selectedBlock">
<MudSelect T="string" ValueChanged="@(name => UpdateSelectedBlock(name))">
@foreach (BlockViewModel block in _blocks)
{
<MudSelectItem Value="@block">@block.Name</MudSelectItem>
<MudSelectItem Value="@block.Name">@block.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
@ -94,7 +94,8 @@ @@ -94,7 +94,8 @@
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText>
<MudDivider Class="mb-6"/>
<MudCalendar T="CalendarItem"
<MudCalendar @ref="_calendar"
T="CalendarItem"
Class="mb-6"
Items="@_template.Items"
ShowMonth="false"
@ -125,6 +126,7 @@ @@ -125,6 +126,7 @@
private BlockGroupViewModel _selectedBlockGroup;
private BlockViewModel _selectedBlock;
private DateTime _selectedBlockStart;
private MudCalendar<CalendarItem> _calendar;
public void Dispose()
{
@ -194,12 +196,22 @@ @@ -194,12 +196,22 @@
End = item.EndTime
};
private async Task UpdateBlockGroupItems(BlockGroupViewModel blockGroup)
private async Task UpdateBlockGroupItems(string blockGroupName)
{
_selectedBlockGroup = blockGroup;
Option<BlockGroupViewModel> maybeBlockGroup = Optional(_blockGroups.Find(bg => bg.Name == blockGroupName));
foreach (var blockGroup in maybeBlockGroup)
{
_selectedBlockGroup = blockGroup;
_blocks.Clear();
_blocks.AddRange(await Mediator.Send(new GetBlocksByBlockGroupId(_selectedBlockGroup.Id), _cts.Token));
}
}
_blocks.Clear();
_blocks.AddRange(await Mediator.Send(new GetBlocksByBlockGroupId(_selectedBlockGroup.Id), _cts.Token));
private void UpdateSelectedBlock(string blockName)
{
_selectedBlock = _blocks.Find(bg => bg.Name == blockName);
InvokeAsync(StateHasChanged);
}
private void AddBlockToTemplate()
@ -223,6 +235,8 @@ @@ -223,6 +235,8 @@
_template.Items.Add(item);
InvokeAsync(() => { _calendar.ScrollToTime(TimeOnly.FromDateTime(item.Start)); });
break;
}
@ -235,6 +249,9 @@ @@ -235,6 +249,9 @@
if (_blockToRemove is not null)
{
_template.Items.Remove(_blockToRemove);
await InvokeAsync(() => { _calendar.ScrollToTime(TimeOnly.FromDateTime(_blockToRemove.Start)); });
_blockToRemove = null;
await InvokeAsync(StateHasChanged);

245
ErsatzTV/Pages/Templates.razor

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
@page "/templates"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Tree
@implements IDisposable
@inject ILogger<Templates> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
@ -30,10 +30,10 @@ @@ -30,10 +30,10 @@
<div class="d-flex">
<MudText>Template Group</MudText>
</div>
<MudSelect @bind-Value="_selectedTemplateGroup">
<MudSelect T="string" ValueChanged="@UpdateSelectedTemplateGroup">
@foreach (TemplateGroupViewModel templateGroup in _templateGroups)
{
<MudSelectItem Value="@templateGroup">@templateGroup.Name</MudSelectItem>
<MudSelectItem Value="@templateGroup.Name">@templateGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
@ -49,42 +49,88 @@ @@ -49,42 +49,88 @@
Add Template
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="TemplateTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="TemplateTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="5">
<MudText>@item.Value.Text</MudText>
</MudItem>
</MudGrid>
<div style="justify-self: end;">
@foreach (int templateId in Optional(item.Value.TemplateId))
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Href="@($"templates/{templateId}")"/>
}
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="@(_ => DeleteItem(item.Value))"/>
</div>
</div>
</BodyContent>
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudStack>
<MudTable Hover="true"
Dense="true"
Class="mt-8"
Items="@_templates"
GroupBy="@_groupDefinition"
GroupHeaderStyle="background-color:var(--mud-palette-appbarbackground)"
RowStyle="background-color:var(--mud-palette-background-gray)"
Filter="new Func<TemplateViewModel,bool>(FilterTemplates)">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col style="width: 60px;"/>
<col/>
<col style="width: 180px;"/>
</MudHidden>
</ColGroup>
<ToolBarContent>
<MudTextField T="string"
ValueChanged="@(s => OnSearch(s))"
Placeholder="Search for templates"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.FilterList"
Clearable="true">
</MudTextField>
</ToolBarContent>
<GroupHeaderTemplate>
<MudTd Class="mud-table-cell-custom-group">
@($"{context.Key}")
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="width: 48px;"></div>
<div style="width: 48px;"></div>
<MudTooltip Text="Delete Template Group">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteTemplateGroup(context.Items?.FirstOrDefault()))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</GroupHeaderTemplate>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd>
<div class="d-flex">
@if (context.Id >= 0)
{
<MudTooltip Text="Edit Template">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"templates/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Copy Template">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
OnClick="@(_ => CopyTemplate(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Template">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteTemplate(context))">
</MudIconButton>
</MudTooltip>
}
else
{
<div style="height: 48px; width: 48px"></div>
}
</div>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
</div>
</MudForm>
@code {
private CancellationTokenSource _cts;
private readonly List<TreeItemData<TemplateTreeItemViewModel>> _treeItems = [];
private readonly List<TemplateViewModel> _templates = [];
private List<TemplateGroupViewModel> _templateGroups = [];
private TemplateGroupViewModel _selectedTemplateGroup;
private string _templateGroupName;
private string _templateName;
private string _searchString;
public void Dispose()
{
@ -103,12 +149,8 @@ @@ -103,12 +149,8 @@
{
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), token);
_treeItems.Clear();
TreeViewModel tree = await Mediator.Send(new GetTemplateTree(), token);
foreach (TreeGroupViewModel group in tree.Groups)
{
_treeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(group) });
}
_templates.Clear();
_templates.AddRange(await Mediator.Send(new GetAllTemplates(), token));
}
catch (OperationCanceledException)
{
@ -116,6 +158,14 @@ @@ -116,6 +158,14 @@
}
}
private readonly TableGroupDefinition<TemplateViewModel> _groupDefinition = new()
{
GroupName = "Group",
Indentation = false,
Expandable = true,
Selector = (e) => e.GroupName
};
private async Task AddTemplateGroup()
{
if (!string.IsNullOrWhiteSpace(_templateGroupName))
@ -130,16 +180,24 @@ @@ -130,16 +180,24 @@
foreach (TemplateGroupViewModel templateGroup in result.RightToSeq())
{
_treeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(templateGroup) });
_treeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
_templateGroupName = null;
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
_selectedTemplateGroup = _templateGroups.Find(tg => tg.Id == templateGroup.Id);
_templates.Clear();
_templates.AddRange(await Mediator.Send(new GetAllTemplates(), _cts.Token));
await InvokeAsync(StateHasChanged);
}
}
}
private void UpdateSelectedTemplateGroup(string templateGroupName)
{
_selectedTemplateGroup = _templateGroups.Find(tg => tg.Name == templateGroupName);
InvokeAsync(StateHasChanged);
}
private async Task AddTemplate()
{
if (_selectedTemplateGroup is not null && !string.IsNullOrWhiteSpace(_templateName))
@ -152,13 +210,10 @@ @@ -152,13 +210,10 @@
Logger.LogError("Unexpected error adding template: {Error}", error.Value);
}
foreach (TemplateViewModel template in result.RightToSeq())
if (result.IsRight)
{
foreach (TemplateTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.TemplateGroupId == _selectedTemplateGroup.Id))
{
item.TreeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(template) });
item.TreeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
}
_templates.Clear();
_templates.AddRange(await Mediator.Send(new GetAllTemplates(), _cts.Token));
_templateName = null;
await InvokeAsync(StateHasChanged);
@ -166,46 +221,88 @@ @@ -166,46 +221,88 @@
}
}
private async Task DeleteItem(TemplateTreeItemViewModel treeItem)
private void OnSearch(string query)
{
_searchString = query;
}
private bool FilterTemplates(TemplateViewModel template) => FilterTemplates(template, _searchString);
private bool FilterTemplates(TemplateViewModel template, string searchString)
{
foreach (int templateGroupId in Optional(treeItem.TemplateGroupId))
if (string.IsNullOrWhiteSpace(searchString))
{
var parameters = new DialogParameters { { "EntityType", "template group" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
return true;
}
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template Group", parameters, options);
DialogResult result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await Mediator.Send(new DeleteTemplateGroup(templateGroupId), _cts.Token);
_treeItems.RemoveAll(i => i.Value?.TemplateGroupId == templateGroupId);
if (_selectedTemplateGroup?.Id == templateGroupId)
{
_selectedTemplateGroup = null;
}
if (template.Name.Contains(searchString, StringComparison.OrdinalIgnoreCase))
{
return true;
}
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
return false;
}
private async Task DeleteTemplateGroup(TemplateViewModel template)
{
if (template is null)
{
return;
}
var parameters = new DialogParameters { { "EntityType", "template group" }, { "EntityName", template.GroupName } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template Group", parameters, options);
DialogResult result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await Mediator.Send(new DeleteTemplateGroup(template.TemplateGroupId), _cts.Token);
if (_selectedTemplateGroup?.Id == template.TemplateGroupId)
{
_selectedTemplateGroup = null;
}
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
_templates.Clear();
_templates.AddRange(await Mediator.Send(new GetAllTemplates(), _cts.Token));
await InvokeAsync(StateHasChanged);
}
}
private async Task CopyTemplate(TemplateViewModel template)
{
var parameters = new DialogParameters { { "TemplateId", template.Id } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<CopyTemplateDialog>("Copy Template", parameters, options);
DialogResult dialogResult = await dialog.Result;
if (dialogResult is { Canceled: false, Data: TemplateViewModel data })
{
NavigationManager.NavigateTo($"templates/{data.Id}");
}
}
foreach (int templateId in Optional(treeItem.TemplateId))
private async Task DeleteTemplate(TemplateViewModel template)
{
if (template is null)
{
var parameters = new DialogParameters { { "EntityType", "template" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
return;
}
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template", parameters, options);
DialogResult result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await Mediator.Send(new DeleteTemplate(templateId), _cts.Token);
foreach (TemplateTreeItemViewModel parent in _treeItems.Map(i => i.Value))
{
parent.TreeItems.RemoveAll(i => i.Value == treeItem);
}
var parameters = new DialogParameters { { "EntityType", "template" }, { "EntityName", template.Name } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
await InvokeAsync(StateHasChanged);
}
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Template", parameters, options);
DialogResult result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await Mediator.Send(new DeleteTemplate(template.Id), _cts.Token);
_templates.Clear();
_templates.AddRange(await Mediator.Send(new GetAllTemplates(), _cts.Token));
await InvokeAsync(StateHasChanged);
}
}

100
ErsatzTV/Shared/CopyTemplateDialog.razor

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject IMediator Mediator
@inject ISnackbar Snackbar
@inject ILogger<CopyScheduleDialog> Logger
<MudDialog>
<DialogContent>
<EditForm Model="@_dummyModel" OnSubmit="@(_ => Submit())">
<div class="d-flex mb-6">
<MudText>Select a group for the new template</MudText>
</div>
<MudSelect @bind-Value="_selectedTemplateGroup" Class="mb-6 mx-4" Label="New Template Group">
@foreach (TemplateGroupViewModel templateGroup in _templateGroups)
{
<MudSelectItem Value="@templateGroup">@templateGroup.Name</MudSelectItem>
}
</MudSelect>
<div class="d-flex mb-6">
<MudText>Enter a name for the new template</MudText>
</div>
<MudTextField T="string" Label="New Template Name"
@bind-Text="@_newName"
Class="mb-6 mx-4">
</MudTextField>
</EditForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="@Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="@Submit">
Copy Template
</MudButton>
</DialogActions>
</MudDialog>
@code {
private CancellationTokenSource _cts;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
[Parameter]
public int TemplateId { get; set; }
private record DummyModel;
private readonly DummyModel _dummyModel = new();
private List<TemplateGroupViewModel> _templateGroups = [];
private TemplateGroupViewModel _selectedTemplateGroup;
private string _newName;
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
protected override async Task OnParametersSetAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), token);
}
catch (OperationCanceledException)
{
// do nothing
}
}
private bool CanSubmit() => _selectedTemplateGroup != null && !string.IsNullOrWhiteSpace(_newName);
private async Task Submit()
{
if (!CanSubmit())
{
return;
}
Either<BaseError, TemplateViewModel> maybeResult =
await Mediator.Send(new CopyTemplate(TemplateId, _selectedTemplateGroup.Id, _newName), _cts.Token);
maybeResult.Match(
schedule => { MudDialog.Close(DialogResult.Ok(schedule)); },
error =>
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Error copying template: {Error}", error.Value);
MudDialog.Close(DialogResult.Cancel());
});
}
private void Cancel(MouseEventArgs e) => MudDialog.Cancel();
}
Loading…
Cancel
Save