refactor(filtering): consolidate task list filters into single strategy registry
Replace the three drifting filter implementations (counter, list loader, regroup) with one ITaskListFilter strategy per list kind. Counter and list loader now share the same predicates, so they cannot diverge again. Planning hierarchy rules (parent-as-context, orphan handling) live in PlanningRules and are unit-tested via 29 new tests in ClaudeDo.Data.Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Filtering;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
@@ -18,6 +19,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
private readonly Dictionary<string, bool> _expandedState = new();
|
||||
private ListNavItemViewModel? _currentList;
|
||||
private CancellationTokenSource? _loadCts;
|
||||
private static readonly TaskListFilterRegistry _filters = new();
|
||||
|
||||
public event EventHandler? SelectionChanged;
|
||||
public event EventHandler? FocusAddTaskRequested;
|
||||
@@ -192,25 +194,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
static bool IsPlanningParent(TaskEntity t) => t.PlanningPhase != PlanningPhase.None;
|
||||
|
||||
IEnumerable<TaskEntity> filtered = list.Kind switch
|
||||
{
|
||||
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
|
||||
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
|
||||
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
|
||||
ListKind.Virtual when list.Id == "virtual:queued" => all.Where(t =>
|
||||
(t.Status == TaskStatus.Queued && t.ParentTaskId == null) ||
|
||||
(IsPlanningParent(t) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Queued))),
|
||||
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t =>
|
||||
(t.Status == TaskStatus.Running && t.ParentTaskId == null) ||
|
||||
(IsPlanningParent(t) && 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 && t.ParentTaskId == null),
|
||||
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
|
||||
_ => Enumerable.Empty<TaskEntity>(),
|
||||
};
|
||||
|
||||
var filteredList = filtered.ToList();
|
||||
var filter = _filters.Resolve(list.Id);
|
||||
var filteredList = filter is null
|
||||
? new List<TaskEntity>()
|
||||
: all.Where(t => filter.Matches(t) || filter.MatchesAsContext(t, all)).ToList();
|
||||
var topIds = filteredList.Where(t => t.ParentTaskId == null).Select(t => t.Id).ToHashSet();
|
||||
var existingIds = filteredList.Select(t => t.Id).ToHashSet();
|
||||
foreach (var c in all.Where(t => t.ParentTaskId != null && topIds.Contains(t.ParentTaskId!)))
|
||||
@@ -282,17 +269,27 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
|
||||
// Build hierarchy-aware flat list: top-level rows interleaved with visible children.
|
||||
// Items is already ordered by SortOrder from the DB query.
|
||||
var topLevel = Items.Where(r => !r.IsChild);
|
||||
// Treat rows whose ParentTaskId is not in the current view as orphans -> top-level.
|
||||
var visibleIds = Items.Select(r => r.Id).ToHashSet();
|
||||
bool IsTopLevel(TaskRowViewModel r) =>
|
||||
!r.IsChild
|
||||
|| string.IsNullOrEmpty(r.ParentTaskId)
|
||||
|| !visibleIds.Contains(r.ParentTaskId!);
|
||||
var topLevel = Items.Where(IsTopLevel);
|
||||
var flat = new List<TaskRowViewModel>();
|
||||
var emitted = new HashSet<string>();
|
||||
foreach (var parent in topLevel)
|
||||
{
|
||||
if (!emitted.Add(parent.Id)) continue;
|
||||
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);
|
||||
flat.AddRange(children);
|
||||
foreach (var c in children)
|
||||
if (emitted.Add(c.Id))
|
||||
flat.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user