refactor(ui): unify list-linked-repo predicate into RepoLinkage
ListsIslandViewModel and SettingsModalViewModel each re-implemented "does this list have a linked WorkingDir" with different whitespace handling. RepoLinkage.IsLinked/IsLinkedInDb is now the single definition; both callers derive from it, closing the whitespace-only WorkingDir gap where the Settings modal disagreed with the ListsIsland banner.
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
// Single definition of "does this list have a linked repo" — a WorkingDir that is set to
|
||||||
|
// something other than blank/whitespace. ListsIslandViewModel and SettingsModalViewModel each
|
||||||
|
// answer this over a different scope (UserLists in memory vs. all Lists rows in the DB) and need
|
||||||
|
// it in different shapes, but both must agree on what "linked" means.
|
||||||
|
public static class RepoLinkage
|
||||||
|
{
|
||||||
|
public static bool IsLinked(string? workingDir) => !string.IsNullOrWhiteSpace(workingDir);
|
||||||
|
|
||||||
|
// EF Core can't translate string.IsNullOrWhiteSpace against Sqlite, so the DB-side check is
|
||||||
|
// spelled out via Trim() instead — same semantics as IsLinked above.
|
||||||
|
public static readonly Expression<Func<ListEntity, bool>> IsLinkedInDb =
|
||||||
|
l => l.WorkingDir != null && l.WorkingDir.Trim() != "";
|
||||||
|
}
|
||||||
@@ -187,12 +187,14 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[ObservableProperty] private ListNavItemViewModel? _selectedList;
|
[ObservableProperty] private ListNavItemViewModel? _selectedList;
|
||||||
|
|
||||||
/// <summary>True whenever no User list has a linked <c>WorkingDir</c> — drives the no-repo
|
/// <summary>True whenever no User list has a linked <c>WorkingDir</c> — drives the no-repo
|
||||||
/// banner. Smart/Virtual lists never count. Recomputed after every load and every
|
/// banner. Smart/Virtual lists never count (scope intentionally narrower than
|
||||||
/// list CRUD / list-settings save; never persisted, no dismiss state.</summary>
|
/// SettingsModalViewModel.HasLinkedRepo, which checks all Lists rows in the DB). Recomputed
|
||||||
|
/// after every load and every list CRUD / list-settings save; never persisted, no dismiss
|
||||||
|
/// state.</summary>
|
||||||
[ObservableProperty] private bool _hasNoLinkedRepo = true;
|
[ObservableProperty] private bool _hasNoLinkedRepo = true;
|
||||||
|
|
||||||
private void RecomputeHasNoLinkedRepo() =>
|
private void RecomputeHasNoLinkedRepo() =>
|
||||||
HasNoLinkedRepo = UserLists.All(r => string.IsNullOrWhiteSpace(r.WorkingDir));
|
HasNoLinkedRepo = UserLists.All(r => !RepoLinkage.IsLinked(r.WorkingDir));
|
||||||
|
|
||||||
public string UserName { get; } = Environment.UserName;
|
public string UserName { get; } = Environment.UserName;
|
||||||
public string MachineName { get; } = Environment.MachineName;
|
public string MachineName { get; } = Environment.MachineName;
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
// True once at least one list has a linked repo (WorkingDir). Drives the repo-hint strip on
|
// True once at least one list has a linked repo (WorkingDir). Drives the repo-hint strip on
|
||||||
// Worktrees/Prime Claude/Session Skills/Berichte — nothing is ever hidden, just annotated.
|
// Worktrees/Prime Claude/Session Skills/Berichte — nothing is ever hidden, just annotated.
|
||||||
|
// Scope intentionally broader than ListsIslandViewModel.HasNoLinkedRepo (all Lists rows in
|
||||||
|
// the DB, not just UserLists).
|
||||||
[ObservableProperty] private bool _hasLinkedRepo;
|
[ObservableProperty] private bool _hasLinkedRepo;
|
||||||
|
|
||||||
// Wired by WindowDialogService to close this modal and open the existing repo-import flow.
|
// Wired by WindowDialogService to close this modal and open the existing repo-import flow.
|
||||||
@@ -152,7 +154,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
||||||
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
HasLinkedRepo = await ctx.Lists.AnyAsync(l => l.WorkingDir != null && l.WorkingDir != "");
|
HasLinkedRepo = await ctx.Lists.AnyAsync(RepoLinkage.IsLinkedInDb);
|
||||||
}
|
}
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,21 @@ public class SettingsModalViewModelTests : IDisposable
|
|||||||
Assert.True(vm.HasLinkedRepo);
|
Assert.True(vm.HasLinkedRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HasLinkedRepo_false_when_a_list_working_dir_is_whitespace_only()
|
||||||
|
{
|
||||||
|
using (var ctx = NewContext())
|
||||||
|
{
|
||||||
|
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "Blank repo", CreatedAt = DateTime.UtcNow, WorkingDir = " " });
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
var vm = MakeVm(new FakeWorker());
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.False(vm.HasLinkedRepo);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedCategory_updates_SelectedIndex_and_ignores_null()
|
public void SelectedCategory_updates_SelectedIndex_and_ignores_null()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user