diff --git a/backend/src/TeleWave.Application/Programming/Groups/UpdateGroup/UpdateGroupCommandHandler.cs b/backend/src/TeleWave.Application/Programming/Groups/UpdateGroup/UpdateGroupCommandHandler.cs
index 2f43630..dce1665 100644
--- a/backend/src/TeleWave.Application/Programming/Groups/UpdateGroup/UpdateGroupCommandHandler.cs
+++ b/backend/src/TeleWave.Application/Programming/Groups/UpdateGroup/UpdateGroupCommandHandler.cs
@@ -34,9 +34,15 @@ public sealed class UpdateGroupCommandHandler(
}
///
- /// Переход в статический режим материализует вычисленный состав: правило домен не понимает,
- /// а группа не должна опустеть от смены режима — на неё уже ссылаются слоты. Обратный переход
- /// домен делает сам (список становится закреплённым).
+ /// Смена режима набора. Домен сбрасывает список позиций — в двух режимах он значит разное,
+ /// — а прикладной слой достраивает то, чего домену знать не положено.
+ ///
+ /// В статическом режиме список и есть состав, поэтому переход туда материализует вычисленный
+ /// правилом: группа не должна опустеть от смены режима — на неё уже ссылаются слоты.
+ ///
+ /// В динамическом состав считает правило, а список хранит только ручные поправки — и начинается
+ /// он пустым. Прежние позиции, перенесённые закреплёнными, отменяли бы сам переход: сузить набор
+ /// правилом было бы нечем.
///
private async Task SwitchModeAsync(
Domain.Programming.Group group,
@@ -44,14 +50,14 @@ public sealed class UpdateGroupCommandHandler(
CancellationToken cancellationToken
)
{
- if (mode == GroupMode.Dynamic)
- {
- group.SetMode(mode);
- return;
- }
+ // Состав считаем до смены режима: после неё правило уже применяется к пустому списку.
+ var composition =
+ mode == GroupMode.Static
+ ? await dynamicResolver.ResolveAsync(group, cancellationToken)
+ : [];
- var composition = await dynamicResolver.ResolveAsync(group, cancellationToken);
group.SetMode(mode);
+
foreach (var element in composition)
group.AddElement(element.Kind, element.Id);
}
diff --git a/backend/src/TeleWave.Domain/Programming/Group.cs b/backend/src/TeleWave.Domain/Programming/Group.cs
index ee3bd44..71697a5 100644
--- a/backend/src/TeleWave.Domain/Programming/Group.cs
+++ b/backend/src/TeleWave.Domain/Programming/Group.cs
@@ -81,22 +81,24 @@ public class Group
/// Вычисленный правилом состав при переходе в статический режим сюда не переносится: домен
/// правило не интерпретирует. Материализует его вызывающая сторона до смены режима.
///
+ ///
+ /// Меняет режим набора. Список позиций при этом **сбрасывается**: в двух режимах он означает
+ /// разное, и переносить его как есть — значит соврать про то, чем группа стала.
+ ///
+ /// Прежний состав, оставленный закреплённым, отменял бы саму суть перехода: правило считало бы
+ /// состав заново, но старые позиции оставались бы в нём навсегда, и «по правилу» на деле
+ /// означало бы «по правилу плюс всё, что было». Сузить набор правилом стало бы невозможно.
+ ///
+ /// Обратный переход наполняет список вычисленным составом — это делает прикладной слой: правило
+ /// живёт там, домен его не интерпретирует.
+ ///
public void SetMode(GroupMode mode)
{
if (Mode == mode)
return;
Mode = mode;
- if (mode == GroupMode.Dynamic)
- {
- foreach (var item in _items)
- item.SetRole(GroupItemRole.Pinned);
- return;
- }
-
- _items.RemoveAll(i => i.Role == GroupItemRole.Excluded);
- foreach (var item in _items)
- item.SetRole(GroupItemRole.Member);
+ _items.Clear();
}
/// Записать пересчитанную статистику.
diff --git a/backend/tests/TeleWave.Application.Tests/Programming/DynamicGroupsTests.cs b/backend/tests/TeleWave.Application.Tests/Programming/DynamicGroupsTests.cs
index 4421b7f..f09c019 100644
--- a/backend/tests/TeleWave.Application.Tests/Programming/DynamicGroupsTests.cs
+++ b/backend/tests/TeleWave.Application.Tests/Programming/DynamicGroupsTests.cs
@@ -29,8 +29,9 @@ public class DynamicGroupsTests
var group = Group.Create("Мультфильмы");
group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
- group.AddElement(GroupElementKind.Show, pinned.Id);
group.SetMode(GroupMode.Dynamic);
+ // Закрепляют уже в динамическом режиме: там список — это поправки к правилу.
+ group.AddElement(GroupElementKind.Show, pinned.Id);
await using (var seed = fixture.New())
{
@@ -97,17 +98,24 @@ public class DynamicGroupsTests
}
[Fact]
- public async Task SwitchToDynamic_KeepsExistingItemsAsPinned()
+ public async Task SwitchToDynamic_DropsTheOldListAndLetsTheRuleDecide()
{
- // Смена режима не должна вымывать из эфира то, что уже стояло в слотах.
+ // Переключили «по правилу» — значит состав считает правило. Прежний список, перенесённый
+ // закреплённым, оставался бы в группе навсегда: сузить набор правилом было бы нечем,
+ // и «динамическая» отличалась бы от статической только названием.
var fixture = new TestDb();
- var show = Show.Create("A", ShowKind.Series);
+ var genre = Genre.Create("Мультфильм", "animation");
+ var byRule = WithGenre(Show.Create("По правилу", ShowKind.Series), genre);
+ var old = Show.Create("Из старого списка", ShowKind.Series);
+
var group = Group.Create("Группа");
- group.AddElement(GroupElementKind.Show, show.Id);
+ group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
+ group.AddElement(GroupElementKind.Show, old.Id);
await using (var seed = fixture.New())
{
- seed.Shows.Add(show);
+ seed.Genres.Add(genre);
+ seed.Shows.AddRange(byRule, old);
seed.Groups.Add(group);
await seed.SaveChangesAsync(CancellationToken.None);
}
@@ -117,7 +125,11 @@ public class DynamicGroupsTests
await using var check = fixture.New();
var stored = check.Groups.Include(g => g.Items).Single();
Assert.Equal(GroupMode.Dynamic, stored.Mode);
- Assert.Equal(GroupItemRole.Pinned, stored.Items.Single().Role);
+ Assert.Empty(stored.Items);
+
+ // В составе — только то, что нашло правило: шоу из прежнего списка под него не подходит.
+ var composition = await ResolveAsync(fixture, group.Id);
+ Assert.Equal([byRule.Id], composition.Select(e => e.Id));
}
[Fact]
diff --git a/backend/tests/TeleWave.Application.Tests/Programming/GetGroupTests.cs b/backend/tests/TeleWave.Application.Tests/Programming/GetGroupTests.cs
index 2b4c298..df95b0e 100644
--- a/backend/tests/TeleWave.Application.Tests/Programming/GetGroupTests.cs
+++ b/backend/tests/TeleWave.Application.Tests/Programming/GetGroupTests.cs
@@ -50,8 +50,9 @@ public class GetGroupTests
var group = Group.Create("Динамическая");
group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
- group.AddElement(GroupElementKind.Show, pinned.Id);
group.SetMode(GroupMode.Dynamic);
+ // Закрепляют уже в динамическом режиме: там список — это поправки к правилу.
+ group.AddElement(GroupElementKind.Show, pinned.Id);
await using (var seed = fixture.New())
{
diff --git a/backend/tests/TeleWave.Domain.Tests/Programming/GroupTests.cs b/backend/tests/TeleWave.Domain.Tests/Programming/GroupTests.cs
index f79cbcc..d55bca6 100644
--- a/backend/tests/TeleWave.Domain.Tests/Programming/GroupTests.cs
+++ b/backend/tests/TeleWave.Domain.Tests/Programming/GroupTests.cs
@@ -103,22 +103,26 @@ public class GroupTests
}
[Fact]
- public void SetMode_ToDynamic_KeepsItemsAsPinned()
+ public void SetMode_ToDynamic_DropsTheOldList()
{
- // Смена режима не должна вымывать из эфира то, что уже стояло в слотах.
+ // Прежний состав, перенесённый закреплённым, отменял бы сам переход: правило считало бы
+ // набор заново, а старые позиции оставались бы в нём навсегда — сузить группу правилом
+ // стало бы нечем.
var group = Group.Create("Боевики");
- var show = Guid.NewGuid();
- group.AddElement(GroupElementKind.Show, show);
+ group.AddElement(GroupElementKind.Show, Guid.NewGuid());
+ group.AddElement(GroupElementKind.Show, Guid.NewGuid());
group.SetMode(GroupMode.Dynamic);
Assert.Equal(GroupMode.Dynamic, group.Mode);
- Assert.Equal(GroupItemRole.Pinned, group.Items.Single().Role);
+ Assert.Empty(group.Items);
}
[Fact]
- public void SetMode_BackToStatic_DropsExclusionsAndUnpins()
+ public void SetMode_BackToStatic_DropsCorrectionsToo()
{
+ // В обратную сторону симметрично: закрепления и исключения — поправки к правилу, и без
+ // правила они бессмысленны. Состав статической группе выдаёт прикладной слой.
var group = Group.Create("Боевики");
group.SetMode(GroupMode.Dynamic);
group.AddElement(GroupElementKind.Show, Guid.NewGuid());
@@ -126,9 +130,7 @@ public class GroupTests
group.SetMode(GroupMode.Static);
- // Исключать в статической группе нечего: состав и есть список.
- var item = Assert.Single(group.Items);
- Assert.Equal(GroupItemRole.Member, item.Role);
+ Assert.Empty(group.Items);
}
[Fact]
diff --git a/docs/tv-scheduler-architecture.md b/docs/tv-scheduler-architecture.md
index 3a76ef5..325e90d 100644
--- a/docs/tv-scheduler-architecture.md
+++ b/docs/tv-scheduler-architecture.md
@@ -210,6 +210,16 @@ GroupItem
Новые поступления не пропадают: фоновая проверка сравнивает библиотеку с фильтром и показывает в UI
«доступно 12 новых позиций», добавление — по кнопке.
+**Смена режима собирает список заново.** В статической группе список — это состав, в динамической —
+только поправки к правилу (закреплено / исключено), и одно и то же значить сразу двумя вещами
+не может. Поэтому переход в «по правилу» **сбрасывает** прежние позиции: перенесённые закреплёнными,
+они остались бы в наборе навсегда, правило считало бы состав «плюс всё, что было», и сузить группу
+им стало бы нечем — динамическая отличалась бы от статической только названием. Обратный переход
+симметричен: список чистится и наполняется тем, что вычислило правило (это делает прикладной слой —
+домен правила не интерпретирует), чтобы группа не опустела от смены режима: на неё уже ссылаются
+слоты. В интерфейсе смена режима подтверждается — состав пересобирается, и знать об этом надо
+до нажатия, а не после.
+
```json
{
"elementKinds": ["show", "collection"],
diff --git a/frontend/src/features/admin/groups/GroupDetail.tsx b/frontend/src/features/admin/groups/GroupDetail.tsx
index e10b779..4a0c629 100644
--- a/frontend/src/features/admin/groups/GroupDetail.tsx
+++ b/frontend/src/features/admin/groups/GroupDetail.tsx
@@ -209,7 +209,12 @@ export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) {
{t('admin.groups.mode.label')}: