feat(ui): add subtask tree view with expand/collapse in task list

Tasks with subtasks show a chevron for inline expand/collapse.
Subtask checkboxes toggle completion state directly. Also sets
Windows AppUserModelID for proper taskbar identity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-16 12:16:22 +02:00
parent 4ca48044db
commit 32bb52875f
5 changed files with 234 additions and 67 deletions

View File

@@ -1,7 +1,11 @@
using System.Collections.ObjectModel;
using Avalonia.Media;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels;
@@ -15,6 +19,11 @@ public partial class TaskItemViewModel : ViewModelBase
[ObservableProperty] private string? _description;
[ObservableProperty] private TaskStatus _status;
[ObservableProperty] private bool _isStarting;
[ObservableProperty] private bool _isExpanded;
[ObservableProperty] private bool _hasSubtasks;
[ObservableProperty] private int _subtaskCount;
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
public string Id { get; }
public string ListId { get; }
@@ -23,9 +32,13 @@ public partial class TaskItemViewModel : ViewModelBase
private readonly Func<string, Task>? _runNow;
private readonly Func<bool> _canRunNow;
private readonly Func<string, Task>? _toggleDone;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private bool _subtasksLoaded;
public TaskItemViewModel(TaskEntity entity, IReadOnlyList<TagEntity> tags,
Func<string, Task>? runNow, Func<bool> canRunNow, Func<string, Task>? toggleDone = null)
Func<string, Task>? runNow, Func<bool> canRunNow,
IDbContextFactory<ClaudeDoDbContext> dbFactory, int subtaskCount,
Func<string, Task>? toggleDone = null)
{
Entity = entity;
Id = entity.Id;
@@ -39,6 +52,9 @@ public partial class TaskItemViewModel : ViewModelBase
_runNow = runNow;
_canRunNow = canRunNow;
_toggleDone = toggleDone;
_dbFactory = dbFactory;
_subtaskCount = subtaskCount;
_hasSubtasks = subtaskCount > 0;
}
public bool IsDone => Status == TaskStatus.Done;
@@ -104,4 +120,55 @@ public partial class TaskItemViewModel : ViewModelBase
if (_toggleDone is not null)
await _toggleDone(Id);
}
[RelayCommand]
private async Task ToggleExpanded()
{
IsExpanded = !IsExpanded;
if (IsExpanded && !_subtasksLoaded)
await LoadSubtasksAsync();
}
private async Task LoadSubtasksAsync()
{
using var context = _dbFactory.CreateDbContext();
var repo = new SubtaskRepository(context);
var entities = await repo.GetByTaskIdAsync(Id);
Subtasks.Clear();
foreach (var e in entities)
Subtasks.Add(SubtaskItemViewModel.From(e));
_subtasksLoaded = true;
}
[RelayCommand]
private async Task ToggleSubtaskDone(string subtaskId)
{
var vm = Subtasks.FirstOrDefault(s => s.Id == subtaskId);
if (vm is null) return;
vm.Completed = !vm.Completed;
using var context = _dbFactory.CreateDbContext();
var entity = await context.Subtasks.FindAsync(subtaskId);
if (entity is not null)
{
entity.Completed = vm.Completed;
await context.SaveChangesAsync();
}
}
public async Task RefreshSubtasksAsync(int newCount)
{
SubtaskCount = newCount;
HasSubtasks = newCount > 0;
if (!HasSubtasks)
{
IsExpanded = false;
Subtasks.Clear();
_subtasksLoaded = false;
}
else if (_subtasksLoaded || IsExpanded)
{
await LoadSubtasksAsync();
}
}
}