fix(ui): warn and block save when a list's working dir isn't a git repo

Reuses RepoScanner.IsRepo (now public) to validate WorkingDir whenever it
changes in ListSettingsModalViewModel — Browse or typing. A missing folder
or a non-repo folder shows an inline warning and gates Save via the same
CanExecute pattern the load-failure gate already uses; an empty WorkingDir
is untouched. Previously this only surfaced as a Failed task deep in
WorktreeManager on first run.

Also localizes the two hardcoded EN strings in the same file (delete
confirm, planning-sessions block) into vm.listSettings.* keys.
This commit is contained in:
mika kuns
2026-09-10 13:32:52 +02:00
parent 752cf0926b
commit 3348736e9d
6 changed files with 69 additions and 7 deletions
@@ -39,6 +39,11 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _workingDir = "";
// Recomputed whenever WorkingDir changes (typing or Browse) — RepoScanner.IsRepo is the same
// check RepoImport uses, so a folder either scans in there or warns here, never neither.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _workingDirWarning = "";
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
// A manual list holds reminders: tasks created here start out manual (TaskEntity.IsManual).
[ObservableProperty] private bool _isManual;
@@ -250,9 +255,20 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
}
}
private bool CanSave() => !IsLoading && !_loadFailed;
private bool CanSave() => !IsLoading && !_loadFailed && (IsFolder || string.IsNullOrEmpty(WorkingDirWarning));
private bool CanDelete() => !IsLoading;
// A manual list without a WorkingDir is untouched (empty is always valid) — this only fires
// for the "picked a folder that turns out not to be a repo" case the audit found: it would
// otherwise only surface as a Failed task the first time the worker tries to use it.
partial void OnWorkingDirChanged(string value)
{
if (string.IsNullOrWhiteSpace(value)) { WorkingDirWarning = ""; return; }
WorkingDirWarning = !Directory.Exists(value)
? Loc.T("vm.listSettings.workingDirMissing")
: RepoScanner.IsRepo(value) ? "" : Loc.T("vm.listSettings.workingDirNotRepo");
}
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
@@ -330,7 +346,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
if (ConfirmAsync is not null)
{
var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone.");
var ok = await ConfirmAsync(Loc.T("vm.listSettings.confirmDeleteList", displayName));
if (!ok) return;
}
@@ -347,7 +363,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|| ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true))
{
if (ShowErrorAsync is not null)
await ShowErrorAsync("This list has planning sessions with child tasks. Discard those first, then delete the list.");
await ShowErrorAsync(Loc.T("vm.listSettings.deleteBlockedPlanning"));
return;
}