Enhance GridPlanner and GenerateGridDialog for improved slot management and user experience
ci / build-backend (push) Successful in 2m30s
ci / build-frontend (push) Successful in 1m7s
ci / tests (push) Successful in 2m58s
ci / sonar (push) Successful in 6m28s

Refactored the GridPlanner class to optimize slot management during grid generation, ensuring more efficient handling of slots. Updated the GenerateGridDialog component to improve its layout and usability, enhancing the overall user experience when interacting with slot lists. These changes contribute to better functionality and accessibility in grid management.
This commit is contained in:
Leonid Pershin
2026-07-27 10:35:52 +03:00
parent b8d6b6aad2
commit 062f4aa200
8 changed files with 1197 additions and 0 deletions
@@ -0,0 +1,182 @@
using TeleWave.Application.Library.Collections.GetCollection;
using TeleWave.Application.Library.Collections.ListCollections;
using TeleWave.Application.Library.Interstitials.ListInterstitialBlocks;
using TeleWave.Application.Library.Interstitials.ListInterstitials;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using Xunit;
namespace TeleWave.Application.Tests.Library;
/// <summary>
/// Справочные списки библиотеки: коллекции и ролики. Считают они немного, но каждое число на экране
/// админа приходит отсюда — сериал внутри франшизы, длительность блока рекламы, статус обработки.
/// </summary>
public class LibraryQueriesTests
{
/// <summary>Шоу с готовыми ассетами: без длительностей в списках нечего показывать.</summary>
private static (Show Show, List<MediaAsset> Assets) Playable(
string name,
ShowKind kind,
int episodes,
int minutes
)
{
var show = Show.Create(name, kind);
var assets = new List<MediaAsset>();
for (var i = 0; i < episodes; i++)
{
var asset = MediaAsset.Register($"{name}-{i}.mkv", ".mkv", MediaSource.Upload);
asset.MarkProcessing();
asset.MarkReady(
new MediaReadyInfo(
TimeSpan.FromMinutes(minutes),
6,
minutes * 10,
1920,
1080,
"h264",
"aac",
$"assets/{name}-{i}"
)
);
assets.Add(asset);
show.AddEpisode(asset.Id);
}
return (show, assets);
}
[Fact]
public async Task Collections_CountUnitsNotParts()
{
var fixture = new TestDb();
var series = Playable("Сериал", ShowKind.Series, 10, 25);
var film = Playable("Фильм", ShowKind.Single, 1, 100);
var collection = Collection.Create("Франшиза", "Описание");
collection.AddShow(film.Show.Id);
collection.AddShow(series.Show.Id);
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange([.. series.Assets, .. film.Assets]);
seed.Shows.AddRange(series.Show, film.Show);
seed.Collections.Add(collection);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var list = await new ListCollectionsQueryHandler(db).Handle(
new ListCollectionsQuery(),
CancellationToken.None
);
var summary = Assert.Single(list);
Assert.Equal("Франшиза", summary.Name);
Assert.Equal(2, summary.ItemCount);
// Единиц больше, чем частей: сериал внутри коллекции разворачивается в свои серии.
Assert.Equal(11, summary.UnitCount);
var detail = await new GetCollectionQueryHandler(db).Handle(
new GetCollectionQuery(collection.Id),
CancellationToken.None
);
Assert.True(detail.IsSuccess);
// Порядок частей — заданный, а не алфавитный: франшиза смотрится по порядку выхода.
Assert.Equal(["Фильм", "Сериал"], detail.Value.Items.Select(i => i.ShowName));
Assert.Equal(10, detail.Value.Items[1].EpisodeCount);
}
[Fact]
public async Task GetCollection_FailsForUnknownId()
{
var fixture = new TestDb();
await using var db = fixture.New();
var result = await new GetCollectionQueryHandler(db).Handle(
new GetCollectionQuery(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
}
[Fact]
public async Task Interstitials_ShowDurationAndStatus()
{
var fixture = new TestDb();
var ready = Playable("Реклама", ShowKind.Interstitial, 1, 1);
// Ролик без медиа: заведён, но файла к нему ещё не приложили.
var empty = Show.Create("Пустой ролик", ShowKind.Interstitial);
var notAClip = Playable("Кино", ShowKind.Single, 1, 100);
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange([.. ready.Assets, .. notAClip.Assets]);
seed.Shows.AddRange(ready.Show, empty, notAClip.Show);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var list = await new ListInterstitialsQueryHandler(db).Handle(
new ListInterstitialsQuery(),
CancellationToken.None
);
// В списке роликов только ролики — полнометражка сюда не попадает.
Assert.Equal(2, list.Count);
var withMedia = list.Single(i => i.Name == "Реклама");
Assert.Equal(MediaAssetStatus.Ready, withMedia.AssetStatus);
Assert.Equal(60, withMedia.DurationSeconds);
var withoutMedia = list.Single(i => i.Name == "Пустой ролик");
Assert.Null(withoutMedia.MediaAssetId);
Assert.Null(withoutMedia.AssetStatus);
Assert.Null(withoutMedia.DurationSeconds);
}
[Fact]
public async Task Blocks_AreCollectionsMadeOfClipsOnly()
{
var fixture = new TestDb();
var first = Playable("Ролик 1", ShowKind.Interstitial, 1, 1);
var second = Playable("Ролик 2", ShowKind.Interstitial, 1, 2);
var film = Playable("Кино", ShowKind.Single, 1, 100);
var block = Collection.Create("Рекламный блок");
block.AddShow(first.Show.Id);
block.AddShow(second.Show.Id);
// Смешанная коллекция — франшиза, а не блок: она живёт на своём экране.
var mixed = Collection.Create("Смешанная");
mixed.AddShow(first.Show.Id);
mixed.AddShow(film.Show.Id);
// Пустая коллекция блоком тоже не считается: ставить в эфир из неё нечего.
var empty = Collection.Create("Пустая");
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange([.. first.Assets, .. second.Assets, .. film.Assets]);
seed.Shows.AddRange(first.Show, second.Show, film.Show);
seed.Collections.AddRange(block, mixed, empty);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var blocks = await new ListInterstitialBlocksQueryHandler(db).Handle(
new ListInterstitialBlocksQuery(),
CancellationToken.None
);
var only = Assert.Single(blocks);
Assert.Equal("Рекламный блок", only.Name);
Assert.Equal(2, only.ItemCount);
Assert.Equal(180, only.DurationSeconds);
}
}