feat(worker,ui): wire EF Core into DI and update all consumers to IDbContextFactory
Worker and App Program.cs: replace SqliteConnectionFactory+SchemaInitializer with AddDbContextFactory<ClaudeDoDbContext> + Database.Migrate(). Repos changed from AddSingleton to AddScoped. All singleton services (QueueService, StaleTaskRecovery, WorktreeManager, TaskRunner) and singleton ViewModels (MainWindowViewModel, TaskDetailViewModel, TaskListViewModel, TaskEditorViewModel) now take IDbContextFactory<ClaudeDoDbContext> and create short-lived contexts per operation. Test infrastructure: DbFixture now uses EF migrations instead of SchemaInitializer; all test classes create contexts via DbFixture.CreateContext(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,20 @@ using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.Views;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ListRepository _listRepo;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly WorkerClient _worker;
|
||||
private readonly Func<ListEditorViewModel> _listEditorFactory;
|
||||
|
||||
@@ -26,14 +28,14 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public StatusBarViewModel StatusBar { get; }
|
||||
|
||||
public MainWindowViewModel(
|
||||
ListRepository listRepo,
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
WorkerClient worker,
|
||||
TaskListViewModel taskList,
|
||||
TaskDetailViewModel taskDetail,
|
||||
StatusBarViewModel statusBar,
|
||||
Func<ListEditorViewModel> listEditorFactory)
|
||||
{
|
||||
_listRepo = listRepo;
|
||||
_dbFactory = dbFactory;
|
||||
_worker = worker;
|
||||
_listEditorFactory = listEditorFactory;
|
||||
TaskList = taskList;
|
||||
@@ -48,7 +50,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var lists = await _listRepo.GetAllAsync();
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
var lists = await listRepo.GetAllAsync();
|
||||
foreach (var l in lists)
|
||||
Lists.Add(new ListItemViewModel(l));
|
||||
}
|
||||
@@ -91,10 +95,12 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
await _listRepo.AddAsync(entity);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
await listRepo.AddAsync(entity);
|
||||
var configEntity = editor.BuildConfig(entity.Id);
|
||||
if (configEntity is not null)
|
||||
await _listRepo.SetConfigAsync(configEntity);
|
||||
await listRepo.SetConfigAsync(configEntity);
|
||||
Lists.Add(new ListItemViewModel(entity));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -107,10 +113,17 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
private async Task EditList()
|
||||
{
|
||||
if (SelectedList is null) return;
|
||||
var existing = await _listRepo.GetByIdAsync(SelectedList.Id);
|
||||
if (existing is null) return;
|
||||
|
||||
var config = await _listRepo.GetConfigAsync(existing.Id);
|
||||
ListEntity? existing;
|
||||
ListConfigEntity? config;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var listRepo = new ListRepository(context);
|
||||
existing = await listRepo.GetByIdAsync(SelectedList.Id);
|
||||
if (existing is null) return;
|
||||
config = await listRepo.GetConfigAsync(existing.Id);
|
||||
}
|
||||
|
||||
var editor = _listEditorFactory();
|
||||
await editor.LoadAgentsAsync(_worker);
|
||||
editor.InitForEdit(existing, config);
|
||||
@@ -125,10 +138,12 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
await _listRepo.UpdateAsync(entity);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
await listRepo.UpdateAsync(entity);
|
||||
var configEntity = editor.BuildConfig(entity.Id);
|
||||
if (configEntity is not null)
|
||||
await _listRepo.SetConfigAsync(configEntity);
|
||||
await listRepo.SetConfigAsync(configEntity);
|
||||
SelectedList.Name = entity.Name;
|
||||
SelectedList.WorkingDir = entity.WorkingDir;
|
||||
SelectedList.DefaultCommitType = entity.DefaultCommitType;
|
||||
@@ -146,7 +161,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
// TODO: confirmation dialog
|
||||
try
|
||||
{
|
||||
await _listRepo.DeleteAsync(SelectedList.Id);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
await listRepo.DeleteAsync(SelectedList.Id);
|
||||
Lists.Remove(SelectedList);
|
||||
SelectedList = null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
@@ -9,18 +10,15 @@ using ClaudeDo.Ui.Helpers;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public partial class TaskDetailViewModel : ViewModelBase
|
||||
{
|
||||
private readonly TaskRepository _taskRepo;
|
||||
private readonly WorktreeRepository _worktreeRepo;
|
||||
private readonly ListRepository _listRepo;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly GitService _git;
|
||||
private readonly WorkerClient _worker;
|
||||
private readonly TagRepository _tagRepo;
|
||||
private readonly SubtaskRepository _subtaskRepo;
|
||||
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string? _description;
|
||||
@@ -62,17 +60,11 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
|
||||
public event Action<string>? TaskChanged;
|
||||
|
||||
public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
|
||||
ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo,
|
||||
SubtaskRepository subtaskRepo)
|
||||
public TaskDetailViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, GitService git, WorkerClient worker)
|
||||
{
|
||||
_taskRepo = taskRepo;
|
||||
_worktreeRepo = worktreeRepo;
|
||||
_listRepo = listRepo;
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
_worker = worker;
|
||||
_tagRepo = tagRepo;
|
||||
_subtaskRepo = subtaskRepo;
|
||||
|
||||
worker.TaskMessageEvent += OnTaskMessage;
|
||||
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
|
||||
@@ -98,8 +90,24 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
var task = await _taskRepo.GetByIdAsync(taskId, ct);
|
||||
if (task is null) return;
|
||||
TaskEntity? task;
|
||||
List<TagEntity> tags;
|
||||
List<SubtaskEntity> subtasks;
|
||||
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var taskRepo = new TaskRepository(context);
|
||||
task = await taskRepo.GetByIdAsync(taskId, ct);
|
||||
if (task is null) return;
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
tags = await taskRepo.GetTagsAsync(taskId, ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
subtasks = await subtaskRepo.GetByTaskIdAsync(taskId, ct);
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (AvailableAgents.Count == 0)
|
||||
@@ -149,14 +157,12 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
Tags.Clear();
|
||||
var tags = await _taskRepo.GetTagsAsync(taskId, ct);
|
||||
foreach (var tag in tags)
|
||||
Tags.Add(tag);
|
||||
|
||||
// Tear down old subtask subscriptions before replacing them.
|
||||
foreach (var old in Subtasks) old.PropertyChanged -= OnSubtaskPropertyChanged;
|
||||
Subtasks.Clear();
|
||||
var subtasks = await _subtaskRepo.GetByTaskIdAsync(taskId, ct);
|
||||
foreach (var s in subtasks)
|
||||
{
|
||||
var vm = SubtaskItemViewModel.From(s);
|
||||
@@ -181,7 +187,9 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
{
|
||||
if (_isLoading || _taskId is null) return;
|
||||
|
||||
var entity = await _taskRepo.GetByIdAsync(_taskId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var entity = await taskRepo.GetByIdAsync(_taskId);
|
||||
if (entity is null) return;
|
||||
|
||||
entity.Title = Title;
|
||||
@@ -196,7 +204,7 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
if (Enum.TryParse<Data.Models.TaskStatus>(StatusChoice, true, out var status))
|
||||
entity.Status = status;
|
||||
|
||||
await _taskRepo.UpdateAsync(entity);
|
||||
await taskRepo.UpdateAsync(entity);
|
||||
StatusText = entity.Status.ToString().ToLowerInvariant();
|
||||
TaskChanged?.Invoke(_taskId);
|
||||
}
|
||||
@@ -207,11 +215,15 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
var name = NewTagInput.Trim();
|
||||
if (string.IsNullOrEmpty(name) || _taskId is null) return;
|
||||
|
||||
var tagId = await _tagRepo.GetOrCreateAsync(name);
|
||||
await _taskRepo.AddTagAsync(_taskId, tagId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var tagRepo = new TagRepository(context);
|
||||
var taskRepo = new TaskRepository(context);
|
||||
|
||||
var tagId = await tagRepo.GetOrCreateAsync(name);
|
||||
await taskRepo.AddTagAsync(_taskId, tagId);
|
||||
|
||||
Tags.Clear();
|
||||
var tags = await _taskRepo.GetTagsAsync(_taskId);
|
||||
var tags = await taskRepo.GetTagsAsync(_taskId);
|
||||
foreach (var tag in tags)
|
||||
Tags.Add(tag);
|
||||
|
||||
@@ -223,7 +235,9 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private async Task RemoveTag(TagEntity tag)
|
||||
{
|
||||
if (_taskId is null) return;
|
||||
await _taskRepo.RemoveTagAsync(_taskId, tag.Id);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
await taskRepo.RemoveTagAsync(_taskId, tag.Id);
|
||||
Tags.Remove(tag);
|
||||
TaskChanged?.Invoke(_taskId);
|
||||
}
|
||||
@@ -241,7 +255,9 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
OrderNum = Subtasks.Count,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
await _subtaskRepo.AddAsync(entity);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
await subtaskRepo.AddAsync(entity);
|
||||
var vm = SubtaskItemViewModel.From(entity);
|
||||
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
||||
Subtasks.Add(vm);
|
||||
@@ -251,7 +267,11 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private async Task RemoveSubtask(SubtaskItemViewModel item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Id))
|
||||
await _subtaskRepo.DeleteAsync(item.Id);
|
||||
{
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
await subtaskRepo.DeleteAsync(item.Id);
|
||||
}
|
||||
item.PropertyChanged -= OnSubtaskPropertyChanged;
|
||||
Subtasks.Remove(item);
|
||||
}
|
||||
@@ -262,7 +282,9 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
if (e.PropertyName is not (nameof(SubtaskItemViewModel.Title) or nameof(SubtaskItemViewModel.Completed))) return;
|
||||
try
|
||||
{
|
||||
await _subtaskRepo.UpdateAsync(new SubtaskEntity
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity
|
||||
{
|
||||
Id = vm.Id,
|
||||
TaskId = _taskId ?? "",
|
||||
@@ -321,7 +343,9 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
|
||||
private async Task LoadWorktreeAsync(string taskId)
|
||||
{
|
||||
var wt = await _worktreeRepo.GetByTaskIdAsync(taskId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
var wt = await wtRepo.GetByTaskIdAsync(taskId);
|
||||
HasWorktree = wt is not null;
|
||||
if (wt is not null)
|
||||
{
|
||||
@@ -378,14 +402,27 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private async Task MergeIntoMainAsync()
|
||||
{
|
||||
if (_taskId is null || _listId is null) return;
|
||||
var wt = await _worktreeRepo.GetByTaskIdAsync(_taskId);
|
||||
var list = await _listRepo.GetByIdAsync(_listId);
|
||||
|
||||
WorktreeEntity? wt;
|
||||
ListEntity? list;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
||||
var listRepo = new ListRepository(context);
|
||||
list = await listRepo.GetByIdAsync(_listId);
|
||||
}
|
||||
if (wt is null || list?.WorkingDir is null) return;
|
||||
|
||||
await _git.MergeFfOnlyAsync(list.WorkingDir, wt.BranchName);
|
||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
||||
await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true);
|
||||
await _worktreeRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Merged);
|
||||
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Merged);
|
||||
}
|
||||
await LoadWorktreeAsync(_taskId);
|
||||
}
|
||||
|
||||
@@ -393,12 +430,25 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private async Task KeepAsBranchAsync()
|
||||
{
|
||||
if (_taskId is null || _listId is null) return;
|
||||
var wt = await _worktreeRepo.GetByTaskIdAsync(_taskId);
|
||||
var list = await _listRepo.GetByIdAsync(_listId);
|
||||
|
||||
WorktreeEntity? wt;
|
||||
ListEntity? list;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
||||
var listRepo = new ListRepository(context);
|
||||
list = await listRepo.GetByIdAsync(_listId);
|
||||
}
|
||||
if (wt is null || list?.WorkingDir is null) return;
|
||||
|
||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
||||
await _worktreeRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Kept);
|
||||
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Kept);
|
||||
}
|
||||
await LoadWorktreeAsync(_taskId);
|
||||
}
|
||||
|
||||
@@ -406,13 +456,26 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
private async Task DiscardAsync()
|
||||
{
|
||||
if (_taskId is null || _listId is null) return;
|
||||
var wt = await _worktreeRepo.GetByTaskIdAsync(_taskId);
|
||||
var list = await _listRepo.GetByIdAsync(_listId);
|
||||
|
||||
WorktreeEntity? wt;
|
||||
ListEntity? list;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
||||
var listRepo = new ListRepository(context);
|
||||
list = await listRepo.GetByIdAsync(_listId);
|
||||
}
|
||||
if (wt is null || list?.WorkingDir is null) return;
|
||||
|
||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
||||
await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true);
|
||||
await _worktreeRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Discarded);
|
||||
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var wtRepo = new WorktreeRepository(context);
|
||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Discarded);
|
||||
}
|
||||
await LoadWorktreeAsync(_taskId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public partial class TaskEditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly SubtaskRepository _subtaskRepo;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string? _description;
|
||||
@@ -40,9 +42,9 @@ public partial class TaskEditorViewModel : ViewModelBase
|
||||
public static string[] StatusChoices { get; } =
|
||||
["manual", "queued"];
|
||||
|
||||
public TaskEditorViewModel(SubtaskRepository subtaskRepo)
|
||||
public TaskEditorViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
{
|
||||
_subtaskRepo = subtaskRepo;
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task LoadAgentsAsync(WorkerClient worker)
|
||||
@@ -116,7 +118,9 @@ public partial class TaskEditorViewModel : ViewModelBase
|
||||
WindowTitle = $"Edit Task: {entity.Title}";
|
||||
|
||||
Subtasks.Clear();
|
||||
var list = await _subtaskRepo.GetByTaskIdAsync(entity.Id, ct);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
var list = await subtaskRepo.GetByTaskIdAsync(entity.Id, ct);
|
||||
foreach (var s in list)
|
||||
Subtasks.Add(SubtaskItemViewModel.From(s));
|
||||
}
|
||||
@@ -196,36 +200,42 @@ public partial class TaskEditorViewModel : ViewModelBase
|
||||
// Persist subtask changes
|
||||
if (_editId is not null)
|
||||
{
|
||||
var existing = await _subtaskRepo.GetByTaskIdAsync(taskId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
var existing = await subtaskRepo.GetByTaskIdAsync(taskId);
|
||||
var existingIds = existing.Select(s => s.Id).ToHashSet();
|
||||
var currentIds = Subtasks.Where(s => s.Id != "").Select(s => s.Id).ToHashSet();
|
||||
|
||||
// Deleted
|
||||
foreach (var id in existingIds.Except(currentIds))
|
||||
await _subtaskRepo.DeleteAsync(id);
|
||||
await subtaskRepo.DeleteAsync(id);
|
||||
|
||||
// Updated
|
||||
foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)))
|
||||
{
|
||||
if (vm.Id == "") continue;
|
||||
if (vm.Title != vm.OriginalTitle || vm.Completed != vm.OriginalCompleted)
|
||||
await _subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
||||
else
|
||||
{
|
||||
// update order_num if position changed
|
||||
var orig = existing.FirstOrDefault(e => e.Id == vm.Id);
|
||||
if (orig is not null && orig.OrderNum != idx)
|
||||
await _subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = orig.CreatedAt });
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = orig.CreatedAt });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added (id == "" means new)
|
||||
foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)).Where(x => x.v.Id == ""))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vm.Title)) continue;
|
||||
var newId = Guid.NewGuid().ToString();
|
||||
await _subtaskRepo.AddAsync(new SubtaskEntity { Id = newId, TaskId = taskId, Title = vm.Title.Trim(), Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)).Where(x => x.v.Id == ""))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(vm.Title)) continue;
|
||||
var newId = Guid.NewGuid().ToString();
|
||||
await subtaskRepo.AddAsync(new SubtaskEntity { Id = newId, TaskId = taskId, Title = vm.Title.Trim(), Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
||||
_tcs.TrySetResult(entity);
|
||||
|
||||
@@ -2,21 +2,21 @@ using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.Views;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public partial class TaskListViewModel : ViewModelBase
|
||||
{
|
||||
private readonly TaskRepository _taskRepo;
|
||||
private readonly TagRepository _tagRepo;
|
||||
private readonly ListRepository _listRepo;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly WorkerClient _worker;
|
||||
private readonly Func<TaskEditorViewModel> _editorFactory;
|
||||
private readonly Action<string> _showMessage;
|
||||
@@ -33,13 +33,10 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
partial void OnSelectedTaskChanged(TaskItemViewModel? value) =>
|
||||
SelectedTaskChanged?.Invoke(value);
|
||||
|
||||
public TaskListViewModel(TaskRepository taskRepo, TagRepository tagRepo,
|
||||
ListRepository listRepo, WorkerClient worker,
|
||||
public TaskListViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient worker,
|
||||
Func<TaskEditorViewModel> editorFactory, Action<string> showMessage)
|
||||
{
|
||||
_taskRepo = taskRepo;
|
||||
_tagRepo = tagRepo;
|
||||
_listRepo = listRepo;
|
||||
_dbFactory = dbFactory;
|
||||
_worker = worker;
|
||||
_editorFactory = editorFactory;
|
||||
_showMessage = showMessage;
|
||||
@@ -77,7 +74,9 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
if (listId is not null)
|
||||
{
|
||||
var list = await _listRepo.GetByIdAsync(listId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
var list = await listRepo.GetByIdAsync(listId);
|
||||
ListName = list?.Name ?? "Tasks";
|
||||
}
|
||||
else
|
||||
@@ -89,10 +88,12 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
var entities = await _taskRepo.GetByListAsync(listId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var entities = await taskRepo.GetByListIdAsync(listId);
|
||||
foreach (var e in entities)
|
||||
{
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(e.Id);
|
||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
}
|
||||
}
|
||||
@@ -110,8 +111,13 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
var title = InlineAddTitle.Trim();
|
||||
if (string.IsNullOrEmpty(title) || CurrentListId is null) return;
|
||||
|
||||
var list = await _listRepo.GetByIdAsync(CurrentListId);
|
||||
var defaultCommitType = list?.DefaultCommitType ?? "chore";
|
||||
string defaultCommitType;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var listRepo = new ListRepository(context);
|
||||
var list = await listRepo.GetByIdAsync(CurrentListId);
|
||||
defaultCommitType = list?.DefaultCommitType ?? "chore";
|
||||
}
|
||||
|
||||
var entity = new TaskEntity
|
||||
{
|
||||
@@ -125,8 +131,10 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
await _taskRepo.AddAsync(entity);
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(entity.Id);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
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);
|
||||
Tasks.Add(vm);
|
||||
SelectedTask = vm;
|
||||
@@ -141,9 +149,13 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
[RelayCommand(CanExecute = nameof(CanAddTask))]
|
||||
private async Task AddTask()
|
||||
{
|
||||
// Get list default commit type
|
||||
var list = await _listRepo.GetByIdAsync(CurrentListId);
|
||||
var defaultCommitType = list?.DefaultCommitType ?? "chore";
|
||||
string defaultCommitType;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var listRepo = new ListRepository(context);
|
||||
var list = await listRepo.GetByIdAsync(CurrentListId);
|
||||
defaultCommitType = list?.DefaultCommitType ?? "chore";
|
||||
}
|
||||
|
||||
var editor = _editorFactory();
|
||||
await editor.LoadAgentsAsync(_worker);
|
||||
@@ -159,15 +171,18 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
await _taskRepo.AddAsync(saved);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var tagRepo = new TagRepository(context);
|
||||
await taskRepo.AddAsync(saved);
|
||||
|
||||
foreach (var tagName in editor.SelectedTagNames)
|
||||
{
|
||||
var tagId = await _tagRepo.GetOrCreateAsync(tagName);
|
||||
await _taskRepo.AddTagAsync(saved.Id, tagId);
|
||||
var tagId = await tagRepo.GetOrCreateAsync(tagName);
|
||||
await taskRepo.AddTagAsync(saved.Id, tagId);
|
||||
}
|
||||
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
|
||||
|
||||
// Auto wake-queue if agent+queued
|
||||
@@ -188,10 +203,17 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
private async Task EditTask()
|
||||
{
|
||||
if (SelectedTask is null || CurrentListId is null) return;
|
||||
var entity = await _taskRepo.GetByIdAsync(SelectedTask.Id);
|
||||
if (entity is null) return;
|
||||
|
||||
var taskTags = await _taskRepo.GetTagsAsync(entity.Id);
|
||||
TaskEntity? entity;
|
||||
List<TagEntity> taskTags;
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var taskRepo = new TaskRepository(context);
|
||||
entity = await taskRepo.GetByIdAsync(SelectedTask.Id);
|
||||
if (entity is null) return;
|
||||
taskTags = await taskRepo.GetTagsAsync(entity.Id);
|
||||
}
|
||||
|
||||
var editor = _editorFactory();
|
||||
await editor.LoadAgentsAsync(_worker);
|
||||
await editor.InitForEditAsync(entity, taskTags);
|
||||
@@ -206,18 +228,21 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
try
|
||||
{
|
||||
await _taskRepo.UpdateAsync(saved);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var tagRepo = new TagRepository(context);
|
||||
await taskRepo.UpdateAsync(saved);
|
||||
|
||||
var existingTags = await _taskRepo.GetTagsAsync(saved.Id);
|
||||
var existingTags = await taskRepo.GetTagsAsync(saved.Id);
|
||||
foreach (var old in existingTags)
|
||||
await _taskRepo.RemoveTagAsync(saved.Id, old.Id);
|
||||
await taskRepo.RemoveTagAsync(saved.Id, old.Id);
|
||||
foreach (var tagName in editor.SelectedTagNames)
|
||||
{
|
||||
var tagId = await _tagRepo.GetOrCreateAsync(tagName);
|
||||
await _taskRepo.AddTagAsync(saved.Id, tagId);
|
||||
var tagId = await tagRepo.GetOrCreateAsync(tagName);
|
||||
await taskRepo.AddTagAsync(saved.Id, tagId);
|
||||
}
|
||||
|
||||
var newTags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
var newTags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
|
||||
SelectedTask.Refresh(saved, newTags);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -232,7 +257,9 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
if (SelectedTask is null) return;
|
||||
try
|
||||
{
|
||||
await _taskRepo.DeleteAsync(SelectedTask.Id);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
await taskRepo.DeleteAsync(SelectedTask.Id);
|
||||
Tasks.Remove(SelectedTask);
|
||||
SelectedTask = null;
|
||||
}
|
||||
@@ -244,14 +271,16 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
public async Task RefreshSingleAsync(string taskId)
|
||||
{
|
||||
var entity = await _taskRepo.GetByIdAsync(taskId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var entity = await taskRepo.GetByIdAsync(taskId);
|
||||
var existing = Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
if (entity is null)
|
||||
{
|
||||
if (existing is not null) Tasks.Remove(existing);
|
||||
return;
|
||||
}
|
||||
var tags = await _taskRepo.GetEffectiveTagsAsync(taskId);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(taskId);
|
||||
if (existing is not null)
|
||||
existing.Refresh(entity, tags);
|
||||
}
|
||||
@@ -270,14 +299,16 @@ public partial class TaskListViewModel : ViewModelBase
|
||||
|
||||
private async Task ToggleDoneAsync(string taskId)
|
||||
{
|
||||
var entity = await _taskRepo.GetByIdAsync(taskId);
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var entity = await taskRepo.GetByIdAsync(taskId);
|
||||
if (entity is null) return;
|
||||
|
||||
entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done;
|
||||
if (entity.Status == TaskStatus.Done)
|
||||
entity.FinishedAt = DateTime.UtcNow;
|
||||
|
||||
await _taskRepo.UpdateAsync(entity);
|
||||
await taskRepo.UpdateAsync(entity);
|
||||
await RefreshSingleAsync(taskId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user