fix(ui): align virtual list semantics and complete planning roll-up coverage

This commit is contained in:
mika kuns
2026-04-24 16:03:27 +02:00
parent ada4d9fd9b
commit 6bdfa73150
2 changed files with 32 additions and 4 deletions

View File

@@ -68,6 +68,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var list = _currentList;
if (list is null) return;
// virtual:queued / virtual:running include Planning parents whose children match,
// which can't be decided from a single entity. Always full-reload in those cases.
if (list.Kind == ListKind.Virtual &&
(list.Id == "virtual:queued" || list.Id == "virtual:running"))
{
LoadForList(list);
return;
}
try
{
await using var db = await _dbFactory.CreateDbContextAsync();
@@ -97,14 +106,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
catch { }
}
// NOTE: virtual:queued/virtual:running cannot be decided by a single entity — a Planning
// parent matches iff any child has the matching status. OnWorkerTaskUpdated handles those
// lists via a full reload rather than the delta path.
private static bool TaskMatchesList(TaskEntity t, ListNavItemViewModel list) => list.Kind switch
{
ListKind.Smart when list.Id == "smart:my-day" => t.IsMyDay,
ListKind.Smart when list.Id == "smart:important" => t.IsStarred,
ListKind.Smart when list.Id == "smart:planned" => t.ScheduledFor != null,
ListKind.Virtual when list.Id == "virtual:queued" => t.Status == TaskStatus.Queued,
ListKind.Virtual when list.Id == "virtual:running" => t.Status == TaskStatus.Running,
ListKind.Virtual when list.Id == "virtual:review" => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active,
ListKind.Virtual when list.Id == "virtual:review" => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active && t.ParentTaskId == null,
ListKind.User => $"user:{t.ListId}" == list.Id,
_ => false,
};
@@ -170,7 +180,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t =>
(t.Status == TaskStatus.Running && t.ParentTaskId == null) ||
(IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Running))),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active && t.ParentTaskId == null),
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
_ => Enumerable.Empty<TaskEntity>(),
};
@@ -213,6 +223,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
foreach (var parent in topLevel)
{
flat.Add(parent);
// Also expand for Done parents so their (Done) children reach the classification
// loop and land in CompletedItems alongside the parent.
if ((parent.IsPlanningParent || parent.Done) && parent.IsExpanded)
{
var children = Items.Where(r => r.ParentTaskId == parent.Id);

View File

@@ -122,6 +122,22 @@ public class TasksIslandRegroupTests : IDisposable
Assert.Contains(vm.Items, r => r.Id == "p1" && !r.IsChild);
}
[Fact]
public async Task VirtualQueued_PlannedParentWithQueuedChild_ParentIsStandaloneRow_ChildIsNot()
{
await SeedPlanningWithChildAsync(
parentStatus: TaskStatus.Planned,
childStatus: TaskStatus.Queued,
parentId: "p1",
childId: "c1");
var vm = BuildViewModel();
await LoadAndWaitAsync(vm, VirtualList("virtual:queued", "Queued"));
Assert.Contains(vm.Items, r => r.Id == "p1" && !r.IsChild);
Assert.DoesNotContain(vm.Items, r => r.Id == "c1" && !r.IsChild);
}
[Fact]
public async Task VirtualRunning_RunningChildOfPlanningParent_IsNotStandaloneRow()
{