feat(ui): TasksIslandViewModel with smart/virtual/user filtering

Uses IDbContextFactory directly (singleton-safe). Adds ToggleDone,
ToggleStar, Select commands. Fixes pre-existing SessionTerminalView
compiled-binding error on Classes property via x:CompileBindings="False".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-20 10:22:11 +02:00
parent f167120c90
commit 0034accb4f
3 changed files with 157 additions and 4 deletions

View File

@@ -1,12 +1,127 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TasksIslandViewModel : ViewModelBase
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private ListNavItemViewModel? _currentList;
public event EventHandler? SelectionChanged;
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
[ObservableProperty] private string _newTaskTitle = "";
[ObservableProperty] private TaskRowViewModel? _selectedTask;
public void LoadForList(ListNavItemViewModel? list) { /* Phase 5 */ }
partial void OnSelectedTaskChanged(TaskRowViewModel? value) =>
[ObservableProperty] private string _headerTitle = "";
[ObservableProperty] private string _headerEyebrow = "";
[ObservableProperty] private string _subtitle = "";
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public async void LoadForList(ListNavItemViewModel? list)
{
_currentList = list;
Items.Clear();
if (list is null) return;
HeaderTitle = list.Name;
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd").ToUpperInvariant();
await using var db = await _dbFactory.CreateDbContextAsync();
var all = await db.Tasks
.Include(t => t.List)
.Include(t => t.Worktree)
.ToListAsync();
IEnumerable<TaskEntity> filtered = list.Kind switch
{
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
_ => Enumerable.Empty<TaskEntity>(),
};
foreach (var t in filtered)
Items.Add(TaskRowViewModel.FromEntity(t));
UpdateSubtitle();
}
private void UpdateSubtitle()
{
var open = Items.Count(i => !i.Done);
var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
Subtitle = $"{open} open · {running} running · {review} in review";
}
[RelayCommand]
private async Task AddAsync()
{
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
var listId = _currentList.Id["user:".Length..];
var entity = new TaskEntity
{
Id = Guid.NewGuid().ToString("N"),
ListId = listId,
Title = NewTaskTitle.Trim(),
CreatedAt = DateTime.UtcNow,
};
await using var db = await _dbFactory.CreateDbContextAsync();
db.Tasks.Add(entity);
await db.SaveChangesAsync();
Items.Insert(0, TaskRowViewModel.FromEntity(entity));
NewTaskTitle = "";
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleDoneAsync(TaskRowViewModel row)
{
row.Done = !row.Done;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
row.Status = entity.Status;
await db.SaveChangesAsync();
}
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleStarAsync(TaskRowViewModel row)
{
row.IsStarred = !row.IsStarred;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.IsStarred = row.IsStarred;
await db.SaveChangesAsync();
}
}
[RelayCommand]
private void Select(TaskRowViewModel row) => SelectedTask = row;
partial void OnSelectedTaskChanged(TaskRowViewModel? value)
{
foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value);
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}