feat(ui): add RepoImportModalViewModel with candidate merge logic
This commit is contained in:
121
src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs
Normal file
121
src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
public sealed partial class RepoImportModalViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly HashSet<string> _existingDirs = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public ObservableCollection<RepoImportItemViewModel> Repos { get; } = new();
|
||||||
|
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
public int CreateCount => Repos.Count(r => r.IsChecked && !r.AlreadyAdded);
|
||||||
|
public bool CanCreate => CreateCount > 0;
|
||||||
|
public string CreateButtonText => $"Create {CreateCount} list(s)";
|
||||||
|
|
||||||
|
public RepoImportModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Repos.Clear();
|
||||||
|
_existingDirs.Clear();
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var lists = new ListRepository(ctx);
|
||||||
|
foreach (var l in await lists.GetAllAsync(ct))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(l.WorkingDir))
|
||||||
|
_existingDirs.Add(l.WorkingDir!);
|
||||||
|
}
|
||||||
|
NotifyCreateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFolders(IEnumerable<string> folders)
|
||||||
|
{
|
||||||
|
var current = new HashSet<string>(
|
||||||
|
Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var folder in folders)
|
||||||
|
{
|
||||||
|
var found = RepoScanner.Scan(folder);
|
||||||
|
foreach (var item in BuildCandidates(found, current, _existingDirs))
|
||||||
|
{
|
||||||
|
item.PropertyChanged += OnItemChanged;
|
||||||
|
Repos.Add(item);
|
||||||
|
current.Add(item.FullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotifyCreateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<RepoImportItemViewModel> BuildCandidates(
|
||||||
|
IEnumerable<RepoCandidate> found,
|
||||||
|
IReadOnlySet<string> currentPaths,
|
||||||
|
IReadOnlySet<string> existingDirs)
|
||||||
|
{
|
||||||
|
var items = new List<RepoImportItemViewModel>();
|
||||||
|
foreach (var c in found)
|
||||||
|
{
|
||||||
|
if (currentPaths.Contains(c.FullPath)) continue;
|
||||||
|
items.Add(new RepoImportItemViewModel
|
||||||
|
{
|
||||||
|
Name = c.Name,
|
||||||
|
FullPath = c.FullPath,
|
||||||
|
AlreadyAdded = existingDirs.Contains(c.FullPath),
|
||||||
|
IsChecked = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(RepoImportItemViewModel.IsChecked))
|
||||||
|
NotifyCreateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyCreateState()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CreateCount));
|
||||||
|
OnPropertyChanged(nameof(CanCreate));
|
||||||
|
OnPropertyChanged(nameof(CreateButtonText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CreateAsync()
|
||||||
|
{
|
||||||
|
var toCreate = Repos.Where(r => r.IsChecked && !r.AlreadyAdded).ToList();
|
||||||
|
if (toCreate.Count > 0)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var lists = new ListRepository(ctx);
|
||||||
|
foreach (var r in toCreate)
|
||||||
|
{
|
||||||
|
await lists.AddAsync(new ListEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
|
Name = r.Name,
|
||||||
|
WorkingDir = r.FullPath,
|
||||||
|
DefaultCommitType = CommitTypeRegistry.DefaultType,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel() => CloseAction?.Invoke();
|
||||||
|
}
|
||||||
48
tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs
Normal file
48
tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests;
|
||||||
|
|
||||||
|
public sealed class RepoImportCandidatesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildCandidates_NewRepo_IsCheckedAndNotAlreadyAdded()
|
||||||
|
{
|
||||||
|
var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
|
||||||
|
var current = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);
|
||||||
|
|
||||||
|
Assert.Single(items);
|
||||||
|
Assert.True(items[0].IsChecked);
|
||||||
|
Assert.False(items[0].AlreadyAdded);
|
||||||
|
Assert.Equal("repo-a", items[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCandidates_ExistingWorkingDir_IsMarkedAlreadyAdded()
|
||||||
|
{
|
||||||
|
var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
|
||||||
|
var current = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var existing = new HashSet<string>(new[] { @"c:\src\repo-a" }, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);
|
||||||
|
|
||||||
|
Assert.Single(items);
|
||||||
|
Assert.True(items[0].AlreadyAdded);
|
||||||
|
Assert.True(items[0].IsChecked); // already-added rows render ticked
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCandidates_SkipsPathsAlreadyShown()
|
||||||
|
{
|
||||||
|
var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
|
||||||
|
var current = new HashSet<string>(new[] { @"c:\src\repo-a" }, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);
|
||||||
|
|
||||||
|
Assert.Empty(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user