feat(ui): repo-import modal — remember folders, search, compact rows, no auto-select
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -26,30 +26,40 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Add folder row -->
|
||||
<Border Grid.Row="1" Padding="16,12,16,4">
|
||||
<Button Content="Add folder…" Click="AddFolderClicked" HorizontalAlignment="Left"/>
|
||||
</Border>
|
||||
<!-- Toolbar: search + folder actions -->
|
||||
<StackPanel Grid.Row="1" Spacing="8" Margin="16,12,16,6">
|
||||
<TextBox Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PlaceholderText="Search repos…"/>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Column="0" Content="Add folder…" Click="AddFolderClicked"/>
|
||||
<Button Grid.Column="2" Content="Forget folders"
|
||||
Command="{Binding ForgetFoldersCommand}"
|
||||
IsVisible="{Binding HasFolders}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Repo checklist -->
|
||||
<ScrollViewer Grid.Row="2" Padding="16,4,16,8">
|
||||
<ScrollViewer Grid.Row="2" Padding="16,2,16,8">
|
||||
<ItemsControl ItemsSource="{Binding Repos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:RepoImportItemViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4">
|
||||
<CheckBox Grid.Column="0"
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" Margin="0,1"
|
||||
IsVisible="{Binding IsVisible}">
|
||||
<CheckBox Grid.Column="0" MinWidth="0"
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}"
|
||||
IsEnabled="{Binding CanToggle}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Margin="6,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource TextBrush}" FontSize="13"/>
|
||||
<TextBlock Text="{Binding FullPath}" Foreground="{DynamicResource TextFaintBrush}"
|
||||
FontFamily="{DynamicResource MonoFont}" FontSize="10"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="(already added)"
|
||||
Foreground="{DynamicResource TextFaintBrush}" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="1" Text="{Binding Name}"
|
||||
Foreground="{DynamicResource TextBrush}" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding FullPath}"
|
||||
Foreground="{DynamicResource TextFaintBrush}"
|
||||
FontFamily="{DynamicResource MonoFont}" FontSize="10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="3" Text="(already added)"
|
||||
Foreground="{DynamicResource TextFaintBrush}" FontSize="10"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
IsVisible="{Binding AlreadyAdded}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -25,6 +25,6 @@ public partial class RepoImportModalView : Window
|
||||
});
|
||||
if (folders.Count == 0) return;
|
||||
|
||||
vm.AddFolders(folders.Select(f => f.Path.LocalPath));
|
||||
await vm.AddFoldersAsync(folders.Select(f => f.Path.LocalPath));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user