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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +91,17 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var entities = await taskRepo.GetByListIdAsync(listId);
|
||||
var taskIds = entities.Select(e => e.Id).ToList();
|
||||
var subtaskCounts = await context.Subtasks
|
||||
.Where(s => taskIds.Contains(s.TaskId))
|
||||
.GroupBy(s => s.TaskId)
|
||||
.ToDictionaryAsync(g => g.Key, g => g.Count());
|
||||
foreach (var e in entities)
|
||||
{
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(e.Id);
|
||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
subtaskCounts.TryGetValue(e.Id, out var count);
|
||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected,
|
||||
_dbFactory, count, ToggleDoneAsync));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -135,7 +142,8 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
var taskRepo = new TaskRepository(context);
|
||||
await taskRepo.AddAsync(entity);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(entity.Id);
|
||||
var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync);
|
||||
var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected,
|
||||
_dbFactory, 0, ToggleDoneAsync);
|
||||
Tasks.Add(vm);
|
||||
SelectedTask = vm;
|
||||
InlineAddTitle = "";
|
||||
@@ -183,7 +191,8 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected,
|
||||
_dbFactory, 0, ToggleDoneAsync));
|
||||
|
||||
// Auto wake-queue if agent+queued
|
||||
if (saved.Status == TaskStatus.Queued &&
|
||||
@@ -282,7 +291,11 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
}
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(taskId);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Refresh(entity, tags);
|
||||
var subtaskCount = await context.Subtasks.CountAsync(s => s.TaskId == taskId);
|
||||
await existing.RefreshSubtasksAsync(subtaskCount);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunNowAsync(string taskId)
|
||||
|
||||
Reference in New Issue
Block a user