feat(ui): collapse parent task rows by default with granular row sync
This commit is contained in:
@@ -301,27 +301,17 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
internal void Regroup()
|
||||
{
|
||||
OverdueItems.Clear();
|
||||
OpenItems.Clear();
|
||||
CompletedItems.Clear();
|
||||
|
||||
// Auto-collapse planning parents whose every child is Done (unless the user
|
||||
// has explicitly toggled the row — saved state wins).
|
||||
// Collapse parents that have children by default, so subtasks stay tucked away until
|
||||
// the user expands the row (an explicit toggle is saved and wins over this default).
|
||||
var childrenByParent = Items
|
||||
.Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId))
|
||||
.GroupBy(r => r.ParentTaskId!)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild
|
||||
&& r.PlanningPhase == PlanningPhase.Finalized
|
||||
&& !r.Done))
|
||||
foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild && !r.Done))
|
||||
{
|
||||
if (_expandedState.ContainsKey(parent.Id)) continue;
|
||||
if (childrenByParent.TryGetValue(parent.Id, out var kids)
|
||||
&& kids.Count > 0
|
||||
&& kids.All(c => c.Status == TaskStatus.Done))
|
||||
{
|
||||
if (childrenByParent.TryGetValue(parent.Id, out var kids) && kids.Count > 0)
|
||||
parent.IsExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore IsExpanded from saved state
|
||||
@@ -362,19 +352,29 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
|
||||
var today = DateTime.Today;
|
||||
var overdue = new List<TaskRowViewModel>();
|
||||
var open = new List<TaskRowViewModel>();
|
||||
var completed = new List<TaskRowViewModel>();
|
||||
foreach (var r in flat)
|
||||
{
|
||||
var underOpenPlanningParent = r.IsChild &&
|
||||
flat.Any(p => p.Id == r.ParentTaskId && p.IsPlanningParent && !p.Done);
|
||||
|
||||
if (r.Done && !underOpenPlanningParent)
|
||||
CompletedItems.Add(r);
|
||||
completed.Add(r);
|
||||
else if (r.ScheduledFor is { } d && d.Date < today)
|
||||
OverdueItems.Add(r);
|
||||
overdue.Add(r);
|
||||
else
|
||||
OpenItems.Add(r);
|
||||
open.Add(r);
|
||||
}
|
||||
|
||||
// Reconcile the bound collections in place (granular Insert/Move/Remove) rather than
|
||||
// Clear+Add, so toggling a parent only touches its own child rows — the ItemsControl
|
||||
// keeps every unchanged container instead of tearing the whole list down on a Reset.
|
||||
SyncCollection(OverdueItems, overdue);
|
||||
SyncCollection(OpenItems, open);
|
||||
SyncCollection(CompletedItems, completed);
|
||||
|
||||
HasOverdue = OverdueItems.Count > 0;
|
||||
HasOpen = OpenItems.Count > 0;
|
||||
HasCompleted = CompletedItems.Count > 0;
|
||||
@@ -500,6 +500,28 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
coll.Move(srcIdx, finalIdx);
|
||||
}
|
||||
|
||||
// Reconcile a bound collection toward a target order using granular Remove/Move/Insert,
|
||||
// so unchanged rows keep their containers (no Reset-driven full re-render).
|
||||
private static void SyncCollection(
|
||||
System.Collections.ObjectModel.ObservableCollection<TaskRowViewModel> dst,
|
||||
List<TaskRowViewModel> target)
|
||||
{
|
||||
var keep = new HashSet<TaskRowViewModel>(target);
|
||||
for (int i = dst.Count - 1; i >= 0; i--)
|
||||
if (!keep.Contains(dst[i]))
|
||||
dst.RemoveAt(i);
|
||||
|
||||
for (int i = 0; i < target.Count; i++)
|
||||
{
|
||||
var item = target[i];
|
||||
if (i < dst.Count && ReferenceEquals(dst[i], item)) continue;
|
||||
|
||||
var cur = dst.IndexOf(item);
|
||||
if (cur >= 0) dst.Move(cur, i);
|
||||
else dst.Insert(i, item);
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.ObjectModel.ObservableCollection<TaskRowViewModel>? SectionFor(TaskRowViewModel row)
|
||||
{
|
||||
if (OverdueItems.Contains(row)) return OverdueItems;
|
||||
|
||||
Reference in New Issue
Block a user