Browse Source

block scheduling ui cleanup (#2316)

* sort block tree views

* fix naming validation for block scheduling

* show deco group name in deco editor

* show block group name in block editor

* show template group name in template editor

* show deco template group name in deco template editor

* fix template rename crash

* fix block rename crash

* fix deco template rename crash
pull/2317/head
Jason Dove 3 days ago committed by GitHub
parent
commit
62e140ec98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Scheduling/BlockViewModel.cs
  3. 25
      ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs
  4. 1
      ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs
  5. 27
      ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs
  6. 1
      ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs
  7. 25
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs
  8. 38
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs
  9. 25
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs
  10. 36
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs
  11. 1
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItems.cs
  12. 22
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs
  13. 6
      ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItems.cs
  14. 25
      ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItemsHandler.cs
  15. 2
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItems.cs
  16. 24
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs
  17. 2
      ErsatzTV.Application/Scheduling/DecoTemplateViewModel.cs
  18. 1
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  19. 27
      ErsatzTV.Application/Scheduling/Mapper.cs
  20. 2
      ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs
  21. 2
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroupsHandler.cs
  22. 4
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroupsHandler.cs
  23. 4
      ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroupsHandler.cs
  24. 2
      ErsatzTV.Application/Scheduling/Queries/GetBlockByIdHandler.cs
  25. 3
      ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupIdHandler.cs
  26. 1
      ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs
  27. 2
      ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs
  28. 2
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateByIdHandler.cs
  29. 4
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs
  30. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplatesByDecoTemplateGroupIdHandler.cs
  31. 5
      ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs
  32. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs
  33. 2
      ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs
  34. 2
      ErsatzTV.Application/Scheduling/Queries/GetTemplateByIdHandler.cs
  35. 3
      ErsatzTV.Application/Scheduling/Queries/GetTemplatesByTemplateGroupIdHandler.cs
  36. 2
      ErsatzTV.Application/Scheduling/TemplateViewModel.cs
  37. 6331
      ErsatzTV.Infrastructure.MySql/Migrations/20250814003609_Fix_TemplateUniqueConstraints.Designer.cs
  38. 76
      ErsatzTV.Infrastructure.MySql/Migrations/20250814003609_Fix_TemplateUniqueConstraints.cs
  39. 8
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  40. 6166
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250814003631_Fix_TemplateUniqueConstraints.Designer.cs
  41. 76
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250814003631_Fix_TemplateUniqueConstraints.cs
  42. 8
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  43. 2
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateConfiguration.cs
  44. 2
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateConfiguration.cs
  45. 12
      ErsatzTV/Pages/BlockEditor.razor
  46. 2
      ErsatzTV/Pages/Blocks.razor
  47. 9
      ErsatzTV/Pages/DecoEditor.razor
  48. 17
      ErsatzTV/Pages/DecoTemplateEditor.razor
  49. 2
      ErsatzTV/Pages/DecoTemplates.razor
  50. 2
      ErsatzTV/Pages/Decos.razor
  51. 17
      ErsatzTV/Pages/TemplateEditor.razor
  52. 2
      ErsatzTV/Pages/Templates.razor
  53. 2
      ErsatzTV/ViewModels/BlockItemsEditViewModel.cs
  54. 1
      ErsatzTV/ViewModels/DecoEditViewModel.cs
  55. 2
      ErsatzTV/ViewModels/DecoTemplateItemsEditViewModel.cs
  56. 2
      ErsatzTV/ViewModels/TemplateItemsEditViewModel.cs

5
CHANGELOG.md

@ -67,6 +67,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -67,6 +67,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- When value is `true`, will add named watermark to list of active watermarks
- When value is `false` and `name` is specified, will remove named watermark from list of active watermarks
- When value is `false` and `name` is not specified, will clear all active watermarks
- Use consistent UI sorting and validation, and fix renaming errors for
- Block groups, blocks
- Template groups, templates
- Deco groups, decos
- Deco template groups, deco templates
## [25.4.0] - 2025-08-05
### Added

2
ErsatzTV.Application/Scheduling/BlockViewModel.cs

@ -2,4 +2,4 @@ using ErsatzTV.Core.Domain.Scheduling; @@ -2,4 +2,4 @@ using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Application.Scheduling;
public record BlockViewModel(int Id, string Name, int Minutes, BlockStopScheduling StopScheduling);
public record BlockViewModel(int Id, int GroupId, string GroupName, string Name, int Minutes, BlockStopScheduling StopScheduling);

25
ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs

@ -13,7 +13,7 @@ public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFacto @@ -13,7 +13,7 @@ public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFacto
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, BlockGroup> validation = await Validate(request);
Validation<BaseError, BlockGroup> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistBlockGroup(dbContext, profile));
}
@ -24,10 +24,23 @@ public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFacto @@ -24,10 +24,23 @@ public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFacto
return Mapper.ProjectToViewModel(blockGroup);
}
private static Task<Validation<BaseError, BlockGroup>> Validate(CreateBlockGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new BlockGroup { Name = name, Blocks = [] }));
private static Task<Validation<BaseError, BlockGroup>> Validate(TvContext dbContext, CreateBlockGroup request) =>
ValidateName(dbContext, request).MapT(name => new BlockGroup { Name = name, Blocks = [] });
private static Validation<BaseError, string> ValidateName(CreateBlockGroup createBlockGroup) =>
createBlockGroup.NotEmpty(x => x.Name)
.Bind(_ => createBlockGroup.NotLongerThan(50)(x => x.Name));
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateBlockGroup createBlockGroup)
{
Validation<BaseError, string> result1 = createBlockGroup.NotEmpty(c => c.Name)
.Bind(_ => createBlockGroup.NotLongerThan(50)(c => c.Name));
int duplicateNameCount = await dbContext.BlockGroups
.CountAsync(ps => ps.Name == createBlockGroup.Name);
var result2 = Optional(duplicateNameCount)
.Where(count => count == 0)
.ToValidation<BaseError>("Block group name must be unique");
return (result1, result2).Apply((_, _) => createBlockGroup.Name);
}
}

1
ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs

@ -21,6 +21,7 @@ public class CreateBlockHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -21,6 +21,7 @@ public class CreateBlockHandler(IDbContextFactory<TvContext> dbContextFactory)
{
await dbContext.Blocks.AddAsync(block);
await dbContext.SaveChangesAsync();
await dbContext.Entry(block).Reference(b => b.BlockGroup).LoadAsync();
return Mapper.ProjectToViewModel(block);
}

27
ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs

@ -13,7 +13,7 @@ public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactor @@ -13,7 +13,7 @@ public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactor
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoGroup> validation = await Validate(request);
Validation<BaseError, DecoGroup> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistDecoGroup(dbContext, profile));
}
@ -24,10 +24,23 @@ public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactor @@ -24,10 +24,23 @@ public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactor
return Mapper.ProjectToViewModel(decoGroup);
}
private static Task<Validation<BaseError, DecoGroup>> Validate(CreateDecoGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new DecoGroup { Name = name, Decos = [] }));
private static Task<Validation<BaseError, DecoGroup>> Validate(TvContext dbContext, CreateDecoGroup request) =>
ValidateName(dbContext, request).MapT(name => new DecoGroup { Name = name, Decos = [] });
private static Validation<BaseError, string> ValidateName(CreateDecoGroup createDecoGroup) =>
createDecoGroup.NotEmpty(x => x.Name)
.Bind(_ => createDecoGroup.NotLongerThan(50)(x => x.Name));
}
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateDecoGroup createDecoGroup)
{
Validation<BaseError, string> result1 = createDecoGroup.NotEmpty(c => c.Name)
.Bind(_ => createDecoGroup.NotLongerThan(50)(c => c.Name));
int duplicateNameCount = await dbContext.DecoGroups
.CountAsync(ps => ps.Name == createDecoGroup.Name);
var result2 = Optional(duplicateNameCount)
.Where(count => count == 0)
.ToValidation<BaseError>("Deco group name must be unique");
return (result1, result2).Apply((_, _) => createDecoGroup.Name);
}
}

1
ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs

@ -21,6 +21,7 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -21,6 +21,7 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
{
await dbContext.Decos.AddAsync(deco);
await dbContext.SaveChangesAsync();
await dbContext.Entry(deco).Reference(d => d.DecoGroup).LoadAsync();
return Mapper.ProjectToViewModel(deco);
}

25
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs

@ -13,7 +13,7 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbConte @@ -13,7 +13,7 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbConte
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplateGroup> validation = await Validate(request);
Validation<BaseError, DecoTemplateGroup> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistDecoTemplateGroup(dbContext, profile));
}
@ -26,10 +26,23 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbConte @@ -26,10 +26,23 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbConte
return Mapper.ProjectToViewModel(decoDecoTemplateGroup);
}
private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(CreateDecoTemplateGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] }));
private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(TvContext dbContext, CreateDecoTemplateGroup request) =>
ValidateName(dbContext, request).MapT(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] });
private static Validation<BaseError, string> ValidateName(CreateDecoTemplateGroup createDecoTemplateGroup) =>
createDecoTemplateGroup.NotEmpty(x => x.Name)
.Bind(_ => createDecoTemplateGroup.NotLongerThan(50)(x => x.Name));
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateDecoTemplateGroup createDecoTemplateGroup)
{
Validation<BaseError, string> result1 = createDecoTemplateGroup.NotEmpty(c => c.Name)
.Bind(_ => createDecoTemplateGroup.NotLongerThan(50)(c => c.Name));
int duplicateNameCount = await dbContext.DecoTemplateGroups
.CountAsync(ps => ps.Name == createDecoTemplateGroup.Name);
var result2 = Optional(duplicateNameCount)
.Where(count => count == 0)
.ToValidation<BaseError>("Deco template group name must be unique");
return (result1, result2).Apply((_, _) => createDecoTemplateGroup.Name);
}
}

38
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs

@ -13,7 +13,7 @@ public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFac @@ -13,7 +13,7 @@ public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFac
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplate> validation = await Validate(request);
Validation<BaseError, DecoTemplate> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistDecoTemplate(dbContext, profile));
}
@ -21,18 +21,34 @@ public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFac @@ -21,18 +21,34 @@ public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFac
{
await dbContext.DecoTemplates.AddAsync(decoTemplate);
await dbContext.SaveChangesAsync();
await dbContext.Entry(decoTemplate).Reference(dt => dt.DecoTemplateGroup).LoadAsync();
return Mapper.ProjectToViewModel(decoTemplate);
}
private static Task<Validation<BaseError, DecoTemplate>> Validate(CreateDecoTemplate request) =>
Task.FromResult(
ValidateName(request).Map(name => new DecoTemplate
{
DecoTemplateGroupId = request.DecoTemplateGroupId,
Name = name
}));
private static async Task<Validation<BaseError, DecoTemplate>> Validate(
TvContext dbContext,
CreateDecoTemplate request) =>
await ValidateDecoTemplateName(dbContext, request).MapT(name => new DecoTemplate
{
DecoTemplateGroupId = request.DecoTemplateGroupId,
Name = name
});
private static Validation<BaseError, string> ValidateName(CreateDecoTemplate createDecoTemplate) =>
createDecoTemplate.NotEmpty(x => x.Name)
.Bind(_ => createDecoTemplate.NotLongerThan(50)(x => x.Name));
private static async Task<Validation<BaseError, string>> ValidateDecoTemplateName(
TvContext dbContext,
CreateDecoTemplate request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Deco template name \"{request.Name}\" is invalid");
}
Option<DecoTemplate> maybeExisting = await dbContext.DecoTemplates
.FirstOrDefaultAsync(r => r.DecoTemplateGroupId == request.DecoTemplateGroupId && r.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A deco template named \"{request.Name}\" already exists in that deco template group")
: Success<BaseError, string>(request.Name);
}
}

25
ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs

@ -13,7 +13,7 @@ public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFa @@ -13,7 +13,7 @@ public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFa
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, TemplateGroup> validation = await Validate(request);
Validation<BaseError, TemplateGroup> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistTemplateGroup(dbContext, profile));
}
@ -26,10 +26,23 @@ public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFa @@ -26,10 +26,23 @@ public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFa
return Mapper.ProjectToViewModel(templateGroup);
}
private static Task<Validation<BaseError, TemplateGroup>> Validate(CreateTemplateGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new TemplateGroup { Name = name, Templates = [] }));
private static Task<Validation<BaseError, TemplateGroup>> Validate(TvContext dbContext, CreateTemplateGroup request) =>
ValidateName(dbContext, request).MapT(name => new TemplateGroup { Name = name, Templates = [] });
private static Validation<BaseError, string> ValidateName(CreateTemplateGroup createTemplateGroup) =>
createTemplateGroup.NotEmpty(x => x.Name)
.Bind(_ => createTemplateGroup.NotLongerThan(50)(x => x.Name));
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateTemplateGroup createTemplateGroup)
{
Validation<BaseError, string> result1 = createTemplateGroup.NotEmpty(c => c.Name)
.Bind(_ => createTemplateGroup.NotLongerThan(50)(c => c.Name));
int duplicateNameCount = await dbContext.TemplateGroups
.CountAsync(ps => ps.Name == createTemplateGroup.Name);
var result2 = Optional(duplicateNameCount)
.Where(count => count == 0)
.ToValidation<BaseError>("Template group name must be unique");
return (result1, result2).Apply((_, _) => createTemplateGroup.Name);
}
}

36
ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs

@ -13,7 +13,7 @@ public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory @@ -13,7 +13,7 @@ public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Template> validation = await Validate(request);
Validation<BaseError, Template> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistTemplate(dbContext, profile));
}
@ -21,18 +21,32 @@ public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory @@ -21,18 +21,32 @@ public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory
{
await dbContext.Templates.AddAsync(template);
await dbContext.SaveChangesAsync();
await dbContext.Entry(template).Reference(t => t.TemplateGroup).LoadAsync();
return Mapper.ProjectToViewModel(template);
}
private static Task<Validation<BaseError, Template>> Validate(CreateTemplate request) =>
Task.FromResult(
ValidateName(request).Map(name => new Template
{
TemplateGroupId = request.TemplateGroupId,
Name = name
}));
private static async Task<Validation<BaseError, Template>> Validate(TvContext dbContext, CreateTemplate request) =>
await ValidateTemplateName(dbContext, request).MapT(name => new Template
{
TemplateGroupId = request.TemplateGroupId,
Name = name
});
private static Validation<BaseError, string> ValidateName(CreateTemplate createTemplate) =>
createTemplate.NotEmpty(x => x.Name)
.Bind(_ => createTemplate.NotLongerThan(50)(x => x.Name));
private static async Task<Validation<BaseError, string>> ValidateTemplateName(
TvContext dbContext,
CreateTemplate request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Template name \"{request.Name}\" is invalid");
}
Option<Template> maybeExisting = await dbContext.Templates
.FirstOrDefaultAsync(r => r.TemplateGroupId == request.TemplateGroupId && r.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A template named \"{request.Name}\" already exists in that template group")
: Success<BaseError, string>(request.Name);
}
}

1
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItems.cs

@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain.Scheduling; @@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceBlockItems(
int BlockGroupId,
int BlockId,
string Name,
int Minutes,

22
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs

@ -61,6 +61,7 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact @@ -61,6 +61,7 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
private static Task<Validation<BaseError, Block>> Validate(TvContext dbContext, ReplaceBlockItems request) =>
BlockMustExist(dbContext, request.BlockId)
.BindT(block => MinutesMustBeValid(request, block))
.BindT(block => BlockNameMustBeValid(dbContext, block, request))
.BindT(block => CollectionTypesMustBeValid(request, block));
private static Task<Validation<BaseError, Block>> BlockMustExist(TvContext dbContext, int blockId) =>
@ -130,4 +131,25 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact @@ -130,4 +131,25 @@ public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFact
return block;
}
private static async Task<Validation<BaseError, Block>> BlockNameMustBeValid(
TvContext dbContext,
Block block,
ReplaceBlockItems request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Block name \"{request.Name}\" is invalid");
}
Option<Block> maybeExisting = await dbContext.Blocks
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.BlockId && d.BlockGroupId == request.BlockGroupId && d.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A block named \"{request.Name}\" already exists in that block group")
: Success<BaseError, Block>(block);
}
}

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

@ -2,5 +2,9 @@ using ErsatzTV.Core; @@ -2,5 +2,9 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceDecoTemplateItems(int DecoTemplateId, string Name, List<ReplaceDecoTemplateItem> Items)
public record ReplaceDecoTemplateItems(
int DecoTemplateId,
int DecoTemplateGroupId,
string Name,
List<ReplaceDecoTemplateItem> Items)
: IRequest<Either<BaseError, List<DecoTemplateItemViewModel>>>;

25
ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItemsHandler.cs

@ -63,7 +63,8 @@ public class ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbCont @@ -63,7 +63,8 @@ public class ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbCont
private static Task<Validation<BaseError, DecoTemplate>> Validate(
TvContext dbContext,
ReplaceDecoTemplateItems request) =>
DecoTemplateMustExist(dbContext, request.DecoTemplateId);
DecoTemplateMustExist(dbContext, request.DecoTemplateId)
.BindT(decoTemplate => DecoTemplateNameMustBeValid(dbContext, decoTemplate, request));
private static Task<Validation<BaseError, DecoTemplate>> DecoTemplateMustExist(
TvContext dbContext,
@ -72,4 +73,26 @@ public class ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbCont @@ -72,4 +73,26 @@ public class ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbCont
.Include(b => b.Items)
.SelectOneAsync(b => b.Id, b => b.Id == decoTemplateId)
.Map(o => o.ToValidation<BaseError>("[DecoTemplateId] does not exist."));
private static async Task<Validation<BaseError, DecoTemplate>> DecoTemplateNameMustBeValid(
TvContext dbContext,
DecoTemplate decoTemplate,
ReplaceDecoTemplateItems request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Deco template name \"{request.Name}\" is invalid");
}
Option<DecoTemplate> maybeExisting = await dbContext.DecoTemplates
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.DecoTemplateId && d.DecoTemplateGroupId == request.DecoTemplateGroupId &&
d.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A deco template named \"{request.Name}\" already exists in that deco template group")
: Success<BaseError, DecoTemplate>(decoTemplate);
}
}

2
ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItems.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core; @@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceTemplateItems(int TemplateId, string Name, List<ReplaceTemplateItem> Items)
public record ReplaceTemplateItems(int TemplateGroupId, int TemplateId, string Name, List<ReplaceTemplateItem> Items)
: IRequest<Either<BaseError, List<TemplateItemViewModel>>>;

24
ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs

@ -56,7 +56,8 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF @@ -56,7 +56,8 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF
private static Task<Validation<BaseError, Template>> Validate(TvContext dbContext, ReplaceTemplateItems request) =>
TemplateMustExist(dbContext, request.TemplateId)
.BindT(template => TemplateItemsMustBeValid(dbContext, template, request));
.BindT(template => TemplateItemsMustBeValid(dbContext, template, request))
.BindT(template => ValidateTemplateName(dbContext, template, request));
private static async Task<Validation<BaseError, Template>> TemplateItemsMustBeValid(
TvContext dbContext,
@ -107,5 +108,26 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF @@ -107,5 +108,26 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF
.SelectOneAsync(b => b.Id, b => b.Id == templateId)
.Map(o => o.ToValidation<BaseError>("[TemplateId] does not exist."));
private static async Task<Validation<BaseError, Template>> ValidateTemplateName(
TvContext dbContext,
Template template,
ReplaceTemplateItems request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Template name \"{request.Name}\" is invalid");
}
Option<Template> maybeExisting = await dbContext.Templates
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.TemplateId && d.TemplateGroupId == request.TemplateGroupId && d.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A template named \"{request.Name}\" already exists in that template group")
: Success<BaseError, Template>(template);
}
private sealed record BlockTemplateItem(int BlockId, TimeSpan StartTime, TimeSpan EndTime);
}

2
ErsatzTV.Application/Scheduling/DecoTemplateViewModel.cs

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoTemplateViewModel(int Id, int DecoTemplateGroupId, string Name);
public record DecoTemplateViewModel(int Id, int DecoTemplateGroupId, string GroupName, string Name);

1
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -7,6 +7,7 @@ namespace ErsatzTV.Application.Scheduling; @@ -7,6 +7,7 @@ namespace ErsatzTV.Application.Scheduling;
public record DecoViewModel(
int Id,
int DecoGroupId,
string DecoGroupName,
string Name,
DecoMode WatermarkMode,
List<WatermarkViewModel> Watermarks,

27
ErsatzTV.Application/Scheduling/Mapper.cs

@ -8,37 +8,37 @@ internal static class Mapper @@ -8,37 +8,37 @@ internal static class Mapper
{
internal static TreeViewModel ProjectToViewModel(List<DecoTemplateGroup> decoTemplateGroups) =>
new(
decoTemplateGroups.Map(dtg => new TreeGroupViewModel(
decoTemplateGroups.OrderBy(dtg => dtg.Name).Map(dtg => new TreeGroupViewModel(
dtg.Id,
dtg.Name,
dtg.DecoTemplates.Map(dt => new TreeItemViewModel(dt.Id, dt.Name)).ToList())).ToList());
dtg.DecoTemplates.OrderBy(dt => dt.Name).Map(dt => new TreeItemViewModel(dt.Id, dt.Name)).ToList())).ToList());
internal static TreeViewModel ProjectToViewModel(List<DecoGroup> decoGroups) =>
new(
decoGroups.Map(dg => new TreeGroupViewModel(
decoGroups.OrderBy(dg => dg.Name).Map(dg => new TreeGroupViewModel(
dg.Id,
dg.Name,
dg.Decos.Map(d => new TreeItemViewModel(d.Id, d.Name)).ToList())).ToList());
dg.Decos.OrderBy(d => d.Name).Map(d => new TreeItemViewModel(d.Id, d.Name)).ToList())).ToList());
internal static TreeViewModel ProjectToViewModel(List<TemplateGroup> templateGroups) =>
new(
templateGroups.Map(tg => new TreeGroupViewModel(
templateGroups.OrderBy(tg => tg.Name).Map(tg => new TreeGroupViewModel(
tg.Id,
tg.Name,
tg.Templates.Map(t => new TreeItemViewModel(t.Id, t.Name)).ToList())).ToList());
tg.Templates.OrderBy(t => t.Name).Map(t => new TreeItemViewModel(t.Id, t.Name)).ToList())).ToList());
internal static BlockTreeViewModel ProjectToViewModel(List<BlockGroup> blockGroups) =>
new(
blockGroups.Map(bg => new BlockTreeBlockGroupViewModel(
blockGroups.OrderBy(bg => bg.Name).Map(bg => new BlockTreeBlockGroupViewModel(
bg.Id,
bg.Name,
bg.Blocks.Map(b => new BlockTreeBlockViewModel(b.Id, b.Name, b.Minutes)).ToList())).ToList());
bg.Blocks.OrderBy(b => b.Name).Map(b => new BlockTreeBlockViewModel(b.Id, b.Name, b.Minutes)).ToList())).ToList());
internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) =>
new(blockGroup.Id, blockGroup.Name);
internal static BlockViewModel ProjectToViewModel(Block block) =>
new(block.Id, block.Name, block.Minutes, block.StopScheduling);
new(block.Id, block.BlockGroupId, block.BlockGroup.Name, block.Name, block.Minutes, block.StopScheduling);
internal static BlockItemViewModel ProjectToViewModel(BlockItem blockItem) =>
new(
@ -67,7 +67,7 @@ internal static class Mapper @@ -67,7 +67,7 @@ internal static class Mapper
new(templateGroup.Id, templateGroup.Name, templateGroup.Templates.Count);
internal static TemplateViewModel ProjectToViewModel(Template template) =>
new(template.Id, template.TemplateGroupId, template.Name);
new(template.Id, template.TemplateGroupId, template.TemplateGroup.Name, template.Name);
internal static TemplateItemViewModel ProjectToViewModel(TemplateItem templateItem)
{
@ -83,6 +83,7 @@ internal static class Mapper @@ -83,6 +83,7 @@ internal static class Mapper
new(
deco.Id,
deco.DecoGroupId,
deco.DecoGroup.Name,
deco.Name,
deco.WatermarkMode,
deco.DecoWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
@ -111,7 +112,11 @@ internal static class Mapper @@ -111,7 +112,11 @@ internal static class Mapper
return null;
}
return new DecoTemplateViewModel(decoTemplate.Id, decoTemplate.DecoTemplateGroupId, decoTemplate.Name);
return new DecoTemplateViewModel(
decoTemplate.Id,
decoTemplate.DecoTemplateGroupId,
decoTemplate.DecoTemplateGroup.Name,
decoTemplate.Name);
}
internal static DecoTemplateItemViewModel ProjectToViewModel(DecoTemplateItem decoTemplateItem)

2
ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs

@ -15,6 +15,6 @@ public class GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFact @@ -15,6 +15,6 @@ public class GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFact
.AsNoTracking()
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
return blockGroups.OrderBy(bg => bg.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

2
ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroupsHandler.cs

@ -16,6 +16,6 @@ public class GetAllDecoGroupsHandler(IDbContextFactory<TvContext> dbContextFacto @@ -16,6 +16,6 @@ public class GetAllDecoGroupsHandler(IDbContextFactory<TvContext> dbContextFacto
.Include(g => g.Decos)
.ToListAsync(cancellationToken);
return decoGroups.Map(Mapper.ProjectToViewModel).ToList();
return decoGroups.OrderBy(dg => dg.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

4
ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroupsHandler.cs

@ -13,11 +13,11 @@ public class GetAllDecoTemplateGroupsHandler(IDbContextFactory<TvContext> dbCont @@ -13,11 +13,11 @@ public class GetAllDecoTemplateGroupsHandler(IDbContextFactory<TvContext> dbCont
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoTemplateGroup> blockGroups = await dbContext.DecoTemplateGroups
List<DecoTemplateGroup> decoTemplateGroups = await dbContext.DecoTemplateGroups
.AsNoTracking()
.Include(g => g.DecoTemplates)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
return decoTemplateGroups.OrderBy(dtg => dtg.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

4
ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroupsHandler.cs

@ -13,11 +13,11 @@ public class GetAllTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextF @@ -13,11 +13,11 @@ public class GetAllTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextF
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<TemplateGroup> blockGroups = await dbContext.TemplateGroups
List<TemplateGroup> templateGroups = await dbContext.TemplateGroups
.AsNoTracking()
.Include(g => g.Templates)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
return templateGroups.OrderBy(tg => tg.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

2
ErsatzTV.Application/Scheduling/Queries/GetBlockByIdHandler.cs

@ -11,6 +11,8 @@ public class GetBlockByIdHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -11,6 +11,8 @@ public class GetBlockByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Blocks
.AsNoTracking()
.Include(b => b.BlockGroup)
.SelectOneAsync(b => b.Id, b => b.Id == request.BlockId)
.MapT(Mapper.ProjectToViewModel);
}

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

@ -14,8 +14,9 @@ public class GetBlocksByBlockGroupIdHandler(IDbContextFactory<TvContext> dbConte @@ -14,8 +14,9 @@ public class GetBlocksByBlockGroupIdHandler(IDbContextFactory<TvContext> dbConte
List<Block> blocks = await dbContext.Blocks
.Filter(b => b.BlockGroupId == request.BlockGroupId)
.AsNoTracking()
.Include(b => b.BlockGroup)
.ToListAsync(cancellationToken);
return blocks.Map(Mapper.ProjectToViewModel).ToList();
return blocks.OrderBy(b => b.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

1
ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs

@ -12,6 +12,7 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -12,6 +12,7 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Decos
.AsNoTracking()
.Include(d => d.DecoGroup)
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoId)

2
ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs

@ -13,6 +13,8 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac @@ -13,6 +13,8 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac
return await dbContext.Playouts
.AsNoTracking()
.Include(p => p.Deco)
.ThenInclude(d => d.DecoGroup)
.Include(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId && p.DecoId != null)

2
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateByIdHandler.cs

@ -13,6 +13,8 @@ public class GetDecoTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFa @@ -13,6 +13,8 @@ public class GetDecoTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFa
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.DecoTemplates
.AsNoTracking()
.Include(dt => dt.DecoTemplateGroup)
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoTemplateId)
.MapT(Mapper.ProjectToViewModel);
}

4
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs

@ -15,11 +15,11 @@ public class GetDecoTemplateTreeHandler(IDbContextFactory<TvContext> dbContextFa @@ -15,11 +15,11 @@ public class GetDecoTemplateTreeHandler(IDbContextFactory<TvContext> dbContextFa
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoTemplateGroup> templateGroups = await dbContext.DecoTemplateGroups
List<DecoTemplateGroup> decoTemplateGroups = await dbContext.DecoTemplateGroups
.AsNoTracking()
.Include(g => g.DecoTemplates)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
return Mapper.ProjectToViewModel(decoTemplateGroups);
}
}

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

@ -15,7 +15,8 @@ public class GetDecoTemplatesByDecoTemplateGroupIdHandler(IDbContextFactory<TvCo @@ -15,7 +15,8 @@ public class GetDecoTemplatesByDecoTemplateGroupIdHandler(IDbContextFactory<TvCo
return await dbContext.DecoTemplates
.AsNoTracking()
.Filter(i => i.DecoTemplateGroupId == request.DecoTemplateGroupId)
.Include(dt => dt.DecoTemplateGroup)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
.Map(items => items.OrderBy(dt => dt.Name).Map(Mapper.ProjectToViewModel).ToList());
}
}

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

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Application.Tree;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
@ -15,11 +14,11 @@ public class GetDecoTreeHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -15,11 +14,11 @@ public class GetDecoTreeHandler(IDbContextFactory<TvContext> dbContextFactory)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoGroup> templateGroups = await dbContext.DecoGroups
List<DecoGroup> deoGroups = await dbContext.DecoGroups
.AsNoTracking()
.Include(g => g.Decos)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
return Mapper.ProjectToViewModel(deoGroups);
}
}

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

@ -13,11 +13,12 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext @@ -13,11 +13,12 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext
List<Deco> decos = await dbContext.Decos
.AsNoTracking()
.Include(d => d.DecoGroup)
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Filter(b => b.DecoGroupId == request.DecoGroupId)
.ToListAsync(cancellationToken);
return decos.Map(Mapper.ProjectToViewModel).ToList();
return decos.OrderBy(d => d.Name).Map(Mapper.ProjectToViewModel).ToList();
}
}

2
ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs

@ -17,7 +17,9 @@ public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFa @@ -17,7 +17,9 @@ public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFa
.AsNoTracking()
.Filter(t => t.PlayoutId == request.PlayoutId)
.Include(t => t.Template)
.ThenInclude(t => t.TemplateGroup)
.Include(t => t.DecoTemplate)
.ThenInclude(dt => dt.DecoTemplateGroup)
.ToListAsync(cancellationToken);
return playoutTemplates.Map(Mapper.ProjectToViewModel).ToList();

2
ErsatzTV.Application/Scheduling/Queries/GetTemplateByIdHandler.cs

@ -11,6 +11,8 @@ public class GetTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactor @@ -11,6 +11,8 @@ public class GetTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactor
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Templates
.AsNoTracking()
.Include(t => t.TemplateGroup)
.SelectOneAsync(b => b.Id, b => b.Id == request.TemplateId)
.MapT(Mapper.ProjectToViewModel);
}

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

@ -15,7 +15,8 @@ public class GetTemplatesByTemplateGroupIdHandler(IDbContextFactory<TvContext> d @@ -15,7 +15,8 @@ public class GetTemplatesByTemplateGroupIdHandler(IDbContextFactory<TvContext> d
return await dbContext.Templates
.AsNoTracking()
.Filter(i => i.TemplateGroupId == request.TemplateGroupId)
.Include(t => t.TemplateGroup)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
.Map(items => items.OrderBy(t => t.Name).Map(Mapper.ProjectToViewModel).ToList());
}
}

2
ErsatzTV.Application/Scheduling/TemplateViewModel.cs

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record TemplateViewModel(int Id, int TemplateGroupId, string Name);
public record TemplateViewModel(int Id, int TemplateGroupId, string GroupName, string Name);

6331
ErsatzTV.Infrastructure.MySql/Migrations/20250814003609_Fix_TemplateUniqueConstraints.Designer.cs generated

File diff suppressed because it is too large Load Diff

76
ErsatzTV.Infrastructure.MySql/Migrations/20250814003609_Fix_TemplateUniqueConstraints.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Fix_TemplateUniqueConstraints : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Template_Name",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_Template_TemplateGroupId",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate");
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId_Name",
table: "Template",
columns: new[] { "TemplateGroupId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId_Name",
table: "DecoTemplate",
columns: new[] { "DecoTemplateGroupId", "Name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Template_TemplateGroupId_Name",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId_Name",
table: "DecoTemplate");
migrationBuilder.CreateIndex(
name: "IX_Template_Name",
table: "Template",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId",
table: "Template",
column: "TemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate",
column: "DecoTemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate",
column: "Name",
unique: true);
}
}
}

8
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -2595,9 +2595,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2595,9 +2595,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasKey("Id");
b.HasIndex("DecoTemplateGroupId");
b.HasIndex("Name")
b.HasIndex("DecoTemplateGroupId", "Name")
.IsUnique();
b.ToTable("DecoTemplate", (string)null);
@ -2775,11 +2773,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2775,11 +2773,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasKey("Id");
b.HasIndex("Name")
b.HasIndex("TemplateGroupId", "Name")
.IsUnique();
b.HasIndex("TemplateGroupId");
b.ToTable("Template", (string)null);
});

6166
ErsatzTV.Infrastructure.Sqlite/Migrations/20250814003631_Fix_TemplateUniqueConstraints.Designer.cs generated

File diff suppressed because it is too large Load Diff

76
ErsatzTV.Infrastructure.Sqlite/Migrations/20250814003631_Fix_TemplateUniqueConstraints.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Fix_TemplateUniqueConstraints : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Template_Name",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_Template_TemplateGroupId",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate");
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId_Name",
table: "Template",
columns: new[] { "TemplateGroupId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId_Name",
table: "DecoTemplate",
columns: new[] { "DecoTemplateGroupId", "Name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Template_TemplateGroupId_Name",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId_Name",
table: "DecoTemplate");
migrationBuilder.CreateIndex(
name: "IX_Template_Name",
table: "Template",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId",
table: "Template",
column: "TemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate",
column: "DecoTemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate",
column: "Name",
unique: true);
}
}
}

8
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -2472,9 +2472,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2472,9 +2472,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasKey("Id");
b.HasIndex("DecoTemplateGroupId");
b.HasIndex("Name")
b.HasIndex("DecoTemplateGroupId", "Name")
.IsUnique();
b.ToTable("DecoTemplate", (string)null);
@ -2642,11 +2640,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2642,11 +2640,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasKey("Id");
b.HasIndex("Name")
b.HasIndex("TemplateGroupId", "Name")
.IsUnique();
b.HasIndex("TemplateGroupId");
b.ToTable("Template", (string)null);
});

2
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateConfiguration.cs

@ -10,7 +10,7 @@ public class DecoTemplateConfiguration : IEntityTypeConfiguration<DecoTemplate> @@ -10,7 +10,7 @@ public class DecoTemplateConfiguration : IEntityTypeConfiguration<DecoTemplate>
{
builder.ToTable("DecoTemplate");
builder.HasIndex(b => b.Name)
builder.HasIndex(d => new { d.DecoTemplateGroupId, d.Name })
.IsUnique();
builder.HasMany(b => b.Items)

2
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateConfiguration.cs

@ -10,7 +10,7 @@ public class TemplateConfiguration : IEntityTypeConfiguration<Template> @@ -10,7 +10,7 @@ public class TemplateConfiguration : IEntityTypeConfiguration<Template>
{
builder.ToTable("Template");
builder.HasIndex(b => b.Name)
builder.HasIndex(d => new { d.TemplateGroupId, d.Name })
.IsUnique();
builder.HasMany(b => b.Items)

12
ErsatzTV/Pages/BlockEditor.razor

@ -40,7 +40,13 @@ @@ -40,7 +40,13 @@
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
<MudText>Block Group Name</MudText>
</div>
<MudTextField @bind-Value="_block.GroupName" Disabled="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Block Name</MudText>
</div>
<MudTextField @bind-Value="_block.Name" For="@(() => _block.Name)"/>
</MudStack>
@ -335,6 +341,8 @@ @@ -335,6 +341,8 @@
{
_block = new BlockItemsEditViewModel
{
GroupId = block.GroupId,
GroupName = block.GroupName,
Name = block.Name,
Minutes = block.Minutes,
StopScheduling = block.StopScheduling,
@ -518,7 +526,7 @@ @@ -518,7 +526,7 @@
_block.Minutes = _durationHours * 60 + _durationMinutes;
return new ReplaceBlockItems(Id, _block.Name, _block.Minutes, _block.StopScheduling, items);
return new ReplaceBlockItems(_block.GroupId, Id, _block.Name, _block.Minutes, _block.StopScheduling, items);
}
private async Task PreviewPlayout()

2
ErsatzTV/Pages/Blocks.razor

@ -130,6 +130,7 @@ @@ -130,6 +130,7 @@
foreach (BlockGroupViewModel blockGroup in result.RightToSeq())
{
_treeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(blockGroup) });
_treeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
_blockGroupName = null;
_blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token);
@ -155,6 +156,7 @@ @@ -155,6 +156,7 @@
foreach (BlockTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.BlockGroupId == _selectedBlockGroup.Id))
{
item.TreeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(block) });
item.TreeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
}
_blockName = null;

9
ErsatzTV/Pages/DecoEditor.razor

@ -24,7 +24,13 @@ @@ -24,7 +24,13 @@
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
<MudText>Deco Group Name</MudText>
</div>
<MudTextField @bind-Value="_deco.GroupName" Disabled="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Name</MudText>
</div>
<MudTextField @bind-Value="_deco.Name" For="@(() => _deco.Name)"/>
</MudStack>
@ -390,6 +396,7 @@ @@ -390,6 +396,7 @@
{
Name = deco.Name,
DecoGroupId = deco.DecoGroupId,
GroupName = deco.DecoGroupName,
WatermarkMode = deco.WatermarkMode,
Watermarks = deco.Watermarks,
UseWatermarkDuringFiller = deco.UseWatermarkDuringFiller,

17
ErsatzTV/Pages/DecoTemplateEditor.razor

@ -19,7 +19,13 @@ @@ -19,7 +19,13 @@
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
<MudText>Deco Template Group Name</MudText>
</div>
<MudTextField @bind-Value="_decoTemplate.GroupName" Disabled="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Template Name</MudText>
</div>
<MudTextField @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/>
</MudStack>
@ -178,7 +184,12 @@ @@ -178,7 +184,12 @@
foreach (DecoTemplateViewModel template in maybeDecoTemplate)
{
_decoTemplate = new DecoTemplateItemsEditViewModel { Name = template.Name };
_decoTemplate = new DecoTemplateItemsEditViewModel
{
GroupId = template.DecoTemplateGroupId,
GroupName = template.GroupName,
Name = template.Name
};
}
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token);
@ -305,7 +316,7 @@ @@ -305,7 +316,7 @@
var items = _decoTemplate.Items.Map(item => new ReplaceDecoTemplateItem(item.DecoId, item.Start.TimeOfDay, item.End!.Value.TimeOfDay)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.Name, items), _cts.Token)
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.GroupId, _decoTemplate.Name, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(

2
ErsatzTV/Pages/DecoTemplates.razor

@ -125,6 +125,7 @@ @@ -125,6 +125,7 @@
foreach (DecoTemplateGroupViewModel decoTemplateGroup in result.RightToSeq())
{
_treeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(decoTemplateGroup) });
_treeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
_decoTemplateGroupName = null;
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
@ -150,6 +151,7 @@ @@ -150,6 +151,7 @@
foreach (DecoTemplateTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.DecoTemplateGroupId == _selectedDecoTemplateGroup.Id))
{
item.TreeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(decoTemplate) });
item.TreeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
}
_decoTemplateName = null;

2
ErsatzTV/Pages/Decos.razor

@ -125,6 +125,7 @@ @@ -125,6 +125,7 @@
foreach (DecoGroupViewModel decoGroup in result.RightToSeq())
{
_treeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(decoGroup) });
_treeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
_decoGroupName = null;
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
@ -150,6 +151,7 @@ @@ -150,6 +151,7 @@
foreach (DecoTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.DecoGroupId == _selectedDecoGroup.Id))
{
item.TreeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(deco) });
item.TreeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture));
}
_decoName = null;

17
ErsatzTV/Pages/TemplateEditor.razor

@ -19,7 +19,13 @@ @@ -19,7 +19,13 @@
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
<MudText>Template Group Name</MudText>
</div>
<MudTextField @bind-Value="_template.GroupName" Disabled="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Template Name</MudText>
</div>
<MudTextField @bind-Value="_template.Name" For="@(() => _template.Name)"/>
</MudStack>
@ -150,7 +156,12 @@ @@ -150,7 +156,12 @@
foreach (TemplateViewModel template in maybeTemplate)
{
_template = new TemplateItemsEditViewModel { Name = template.Name };
_template = new TemplateItemsEditViewModel
{
GroupId = template.TemplateGroupId,
GroupName = template.GroupName,
Name = template.Name
};
}
Option<IEnumerable<TemplateItemViewModel>> maybeResults = await Mediator.Send(new GetTemplateItems(Id), _cts.Token);
@ -271,7 +282,7 @@ @@ -271,7 +282,7 @@
{
var items = _template.Items.Map(item => new ReplaceTemplateItem(item.BlockId, item.Start.TimeOfDay)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceTemplateItems(Id, _template.Name, items), _cts.Token)
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceTemplateItems(_template.GroupId, Id, _template.Name, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(

2
ErsatzTV/Pages/Templates.razor

@ -125,6 +125,7 @@ @@ -125,6 +125,7 @@
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);
@ -150,6 +151,7 @@ @@ -150,6 +151,7 @@
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));
}
_templateName = null;

2
ErsatzTV/ViewModels/BlockItemsEditViewModel.cs

@ -4,6 +4,8 @@ namespace ErsatzTV.ViewModels; @@ -4,6 +4,8 @@ namespace ErsatzTV.ViewModels;
public class BlockItemsEditViewModel
{
public int GroupId { get; set; }
public string GroupName { get; set; }
public string Name { get; set; }
public int Minutes { get; set; }
public BlockStopScheduling StopScheduling { get; set; }

1
ErsatzTV/ViewModels/DecoEditViewModel.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.ViewModels; @@ -9,6 +9,7 @@ namespace ErsatzTV.ViewModels;
public class DecoEditViewModel
{
public int DecoGroupId { get; set; }
public string GroupName { get; set; }
public string Name { get; set; }
public DecoMode WatermarkMode { get; set; }
public IEnumerable<WatermarkViewModel> Watermarks { get; set; }

2
ErsatzTV/ViewModels/DecoTemplateItemsEditViewModel.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
public class DecoTemplateItemsEditViewModel
{
public int GroupId { get; set; }
public string GroupName { get; set; }
public string Name { get; set; }
public List<DecoTemplateItemEditViewModel> Items { get; } = [];
}

2
ErsatzTV/ViewModels/TemplateItemsEditViewModel.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
public class TemplateItemsEditViewModel
{
public int GroupId { get; set; }
public string GroupName { get; set; }
public string Name { get; set; }
public List<TemplateItemEditViewModel> Items { get; } = [];
}

Loading…
Cancel
Save