feat(ui): My Day actions, orphan-aware grouping, menu restructure

Pending UI work:
- My Day add/remove context actions on task rows (parent removal cascades to children)
- orphan-aware grouping: a child whose parent isn't in view renders as a top-level row, not an indented draft
- shell menu restructure (Worker / Repositories submenus); 'Finalize plan' action, drop 'Queue subtasks sequentially'
- notes editor refinements
- subtask-row hover tweak (Surface3, no transition)
- bump Avalonia 12.0.0 -> 12.0.4
This commit is contained in:
Mika Kuns
2026-06-18 16:22:29 +02:00
parent 43fb506e87
commit 4847c5c0a4
19 changed files with 384 additions and 58 deletions
@@ -334,6 +334,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
// Items is already ordered by SortOrder from the DB query.
// Treat rows whose ParentTaskId is not in the current view as orphans -> top-level.
var visibleIds = Items.Select(r => r.Id).ToHashSet();
// A child reads as a child only while its parent is in the view. Flag orphans so they
// render flat (no indent, no Draft/Planned badge) instead of breaking the layout.
foreach (var r in Items)
r.ParentInView = string.IsNullOrEmpty(r.ParentTaskId) || visibleIds.Contains(r.ParentTaskId!);
bool IsTopLevel(TaskRowViewModel r) =>
!r.IsChild
|| string.IsNullOrEmpty(r.ParentTaskId)
@@ -571,6 +575,52 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
TasksChanged?.Invoke(this, EventArgs.Empty);
}
[RelayCommand]
private async Task AddToMyDayAsync(TaskRowViewModel? row)
{
if (row is null || row.IsMyDay) return;
row.IsMyDay = true;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.IsMyDay = true;
await db.SaveChangesAsync();
}
Regroup();
UpdateSubtitle();
TasksChanged?.Invoke(this, EventArgs.Empty);
}
[RelayCommand]
private async Task RemoveFromMyDayAsync(TaskRowViewModel? row)
{
if (row is null) return;
row.IsMyDay = false;
await using var db = await _dbFactory.CreateDbContextAsync();
// Removing a parent takes its whole plan off My Day: clear the task and every child, so no
// orphaned child is left behind (independently-IsMyDay children included). A leaf child has
// no children of its own, so this collapses to just clearing the row itself.
var affected = await db.Tasks
.Where(t => t.Id == row.Id || t.ParentTaskId == row.Id)
.ToListAsync();
foreach (var t in affected)
t.IsMyDay = false;
if (affected.Count > 0)
await db.SaveChangesAsync();
if (_currentList?.Id == "smart:my-day")
{
var drop = Items
.Where(r => r.Id == row.Id || r.ParentTaskId == row.Id)
.ToList();
foreach (var r in drop)
Items.Remove(r);
}
Regroup();
UpdateSubtitle();
TasksChanged?.Invoke(this, EventArgs.Empty);
}
public async Task SetStatusOnRowAsync(TaskRowViewModel row, TaskStatus status)
{
if (_worker is null) return;
@@ -582,6 +632,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
private async Task SendToQueueAsync(TaskRowViewModel? row)
{
if (row is null || row.IsRunning) return;
// A finalized planning parent queues its plan (children sequentially), not itself.
if (row.CanQueuePlan)
{
await QueuePlanningSubtasksAsync(row);
return;
}
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity is null) return;