Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs
mika kuns 32bb52875f 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>
2026-04-16 12:16:22 +02:00

175 lines
5.6 KiB
C#

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;
public partial class TaskItemViewModel : ViewModelBase
{
[ObservableProperty] private string _title;
[ObservableProperty] private string _statusText;
[ObservableProperty] private string _tagsText;
[ObservableProperty] private string _commitType;
[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; }
public TaskEntity Entity { get; private set; }
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,
IDbContextFactory<ClaudeDoDbContext> dbFactory, int subtaskCount,
Func<string, Task>? toggleDone = null)
{
Entity = entity;
Id = entity.Id;
ListId = entity.ListId;
_title = entity.Title;
_status = entity.Status;
_statusText = entity.Status.ToString().ToLowerInvariant();
_tagsText = string.Join(", ", tags.Select(t => t.Name));
_commitType = entity.CommitType;
_description = entity.Description;
_runNow = runNow;
_canRunNow = canRunNow;
_toggleDone = toggleDone;
_dbFactory = dbFactory;
_subtaskCount = subtaskCount;
_hasSubtasks = subtaskCount > 0;
}
public bool IsDone => Status == TaskStatus.Done;
public bool IsRunning => Status == TaskStatus.Running;
public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed;
public TextDecorationCollection? TitleDecorations => IsDone
? TextDecorations.Strikethrough
: null;
public IBrush TitleForeground => IsDone
? new SolidColorBrush(Color.Parse("#5a6578"))
: new SolidColorBrush(Color.Parse("#e2e8f0"));
public double RowOpacity => IsDone ? 0.6 : 1.0;
public void Refresh(TaskEntity entity, IReadOnlyList<TagEntity> tags)
{
Entity = entity;
Title = entity.Title;
Status = entity.Status;
StatusText = entity.Status.ToString().ToLowerInvariant();
TagsText = string.Join(", ", tags.Select(t => t.Name));
CommitType = entity.CommitType;
Description = entity.Description;
RunNowCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsRunning));
IsStarting = false;
OnPropertyChanged(nameof(CanToggleDone));
OnPropertyChanged(nameof(TitleDecorations));
OnPropertyChanged(nameof(TitleForeground));
OnPropertyChanged(nameof(RowOpacity));
ToggleDoneCommand.NotifyCanExecuteChanged();
}
public void SetStarting()
{
IsStarting = true;
StatusText = "starting...";
RunNowCommand.NotifyCanExecuteChanged();
}
public void ClearStarting()
{
IsStarting = false;
RunNowCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanRunNow))]
private async Task RunNowAsync()
{
if (_runNow is not null)
await _runNow(Id);
}
private bool CanRunNow() =>
_canRunNow() && Status != TaskStatus.Running && !IsStarting;
[RelayCommand(CanExecute = nameof(CanToggleDone))]
private async Task ToggleDone()
{
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();
}
}
}