feat(ui): repo-import modal — remember folders, search, compact rows, no auto-select

This commit is contained in:
mika kuns
2026-05-29 16:29:22 +02:00
parent bb8b3e235a
commit 6d0973c67c
10 changed files with 181 additions and 26 deletions

View File

@@ -12,4 +12,7 @@ public sealed partial class RepoImportItemViewModel : ViewModelBase
public bool CanToggle => !AlreadyAdded;
[ObservableProperty] private bool _isChecked;
// Driven by the search filter; the row collapses when it doesn't match.
[ObservableProperty] private bool _isVisible = true;
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -13,14 +14,18 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly HashSet<string> _existingDirs = new(StringComparer.OrdinalIgnoreCase);
private readonly List<string> _folders = new();
public ObservableCollection<RepoImportItemViewModel> Repos { get; } = new();
public Action? CloseAction { get; set; }
[ObservableProperty] private string _searchText = "";
public int CreateCount => Repos.Count(r => r.IsChecked && !r.AlreadyAdded);
public bool CanCreate => CreateCount > 0;
public string CreateButtonText => $"Create {CreateCount} list(s)";
public bool HasFolders => _folders.Count > 0;
public RepoImportModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
@@ -29,8 +34,9 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
public async Task LoadAsync(CancellationToken ct = default)
{
Repos.Clear();
ClearRepos();
_existingDirs.Clear();
_folders.Clear();
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
var lists = new ListRepository(ctx);
@@ -39,25 +45,45 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
if (!string.IsNullOrWhiteSpace(l.WorkingDir))
_existingDirs.Add(l.WorkingDir!);
}
var settings = new AppSettingsRepository(ctx);
foreach (var f in await settings.GetRepoImportFoldersAsync(ct))
AddFolderToSet(f);
ScanAndAdd(_folders);
OnPropertyChanged(nameof(HasFolders));
NotifyCreateState();
}
public void AddFolders(IEnumerable<string> folders)
public async Task AddFoldersAsync(IEnumerable<string> folders)
{
var added = new List<string>();
foreach (var f in folders)
if (AddFolderToSet(f)) added.Add(f);
if (added.Count == 0) return;
ScanAndAdd(added);
OnPropertyChanged(nameof(HasFolders));
NotifyCreateState();
await SaveFoldersAsync();
}
private void ScanAndAdd(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))
foreach (var item in BuildCandidates(RepoScanner.Scan(folder), current, _existingDirs))
{
item.PropertyChanged += OnItemChanged;
Repos.Add(item);
current.Add(item.FullPath);
}
}
NotifyCreateState();
ApplyFilter();
}
public static List<RepoImportItemViewModel> BuildCandidates(
@@ -69,17 +95,60 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
foreach (var c in found)
{
if (currentPaths.Contains(c.FullPath)) continue;
var alreadyAdded = existingDirs.Contains(c.FullPath);
items.Add(new RepoImportItemViewModel
{
Name = c.Name,
FullPath = c.FullPath,
AlreadyAdded = existingDirs.Contains(c.FullPath),
IsChecked = true,
AlreadyAdded = alreadyAdded,
IsChecked = alreadyAdded,
});
}
return items;
}
[RelayCommand]
private async Task ForgetFoldersAsync()
{
_folders.Clear();
ClearRepos();
OnPropertyChanged(nameof(HasFolders));
NotifyCreateState();
await SaveFoldersAsync();
}
partial void OnSearchTextChanged(string value) => ApplyFilter();
private void ApplyFilter()
{
var q = SearchText?.Trim() ?? "";
foreach (var r in Repos)
r.IsVisible = q.Length == 0
|| r.Name.Contains(q, StringComparison.OrdinalIgnoreCase)
|| r.FullPath.Contains(q, StringComparison.OrdinalIgnoreCase);
}
private bool AddFolderToSet(string folder)
{
if (string.IsNullOrWhiteSpace(folder)) return false;
if (_folders.Any(f => string.Equals(f, folder, StringComparison.OrdinalIgnoreCase)))
return false;
_folders.Add(folder);
return true;
}
private async Task SaveFoldersAsync()
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
await new AppSettingsRepository(ctx).SetRepoImportFoldersAsync(_folders);
}
private void ClearRepos()
{
foreach (var r in Repos) r.PropertyChanged -= OnItemChanged;
Repos.Clear();
}
private void OnItemChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(RepoImportItemViewModel.IsChecked))