refactor(ui): single IDialogService replaces scattered Show* dialog seams

Collapses the ~10 per-modal Show*Modal Func callbacks (wired separately on the shell and the lists island) into one IDialogService + WindowDialogService impl. Removes the RepoImport/WorktreesOverview dialog construction duplicated across MainWindow and ListsIslandView, plus the Confirm/Error dialogs duplicated in both code-behinds. Shell/lists Open* commands now route through an injected Dialogs handle (propagated shell->lists); the per-list worktrees overview also wires conflict resolution now, matching the global one. No VM ctor changes (Dialogs is a settable seam), so no test-fake impact.
This commit is contained in:
Mika Kuns
2026-06-22 16:52:50 +02:00
parent 1fb2e34f85
commit d598a539bc
6 changed files with 234 additions and 291 deletions

View File

@@ -27,28 +27,25 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
public event EventHandler? FocusSearchRequested;
public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty);
public Func<SettingsModalViewModel, Task>? ShowSettingsModal { get; set; }
public Func<ListSettingsModalViewModel, System.Threading.Tasks.Task>? ShowListSettingsModal { get; set; }
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; }
public Func<RepoImportModalViewModel, System.Threading.Tasks.Task>? ShowRepoImportModal { get; set; }
public IDialogService? Dialogs { get; set; }
[RelayCommand]
private async Task OpenSettings()
{
if (ShowSettingsModal is null || _services is null) return;
if (Dialogs is null || _services is null) return;
var settingsVm = _services.GetRequiredService<SettingsModalViewModel>();
await settingsVm.LoadAsync();
await ShowSettingsModal(settingsVm);
await Dialogs.ShowSettingsAsync(settingsVm);
}
[RelayCommand]
private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row)
{
if (row is null || ShowListSettingsModal is null || _services is null) return;
if (row is null || Dialogs is null || _services is null) return;
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType);
await ShowListSettingsModal(vm);
await Dialogs.ShowListSettingsAsync(vm);
if (vm.Deleted) await LoadAsync();
else await RefreshRowAsync(row.Id);
}
@@ -56,10 +53,10 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async System.Threading.Tasks.Task OpenRepoImportAsync()
{
if (ShowRepoImportModal is null || _services is null) return;
if (Dialogs is null || _services is null) return;
var vm = _services.GetRequiredService<RepoImportModalViewModel>();
await vm.LoadAsync();
await ShowRepoImportModal(vm);
await Dialogs.ShowRepoImportAsync(vm);
await LoadAsync();
}
@@ -68,7 +65,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async Task OpenWorktreesOverviewAsync(ListNavItemViewModel? row)
{
if (row is null || ShowWorktreesOverviewModal is null || _services is null) return;
if (row is null || Dialogs is null || _services is null) return;
if (row.Kind != ListKind.User) return;
if (_worktreesOverviewOpen) return;
_worktreesOverviewOpen = true;
@@ -78,7 +75,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
var vm = _services.GetRequiredService<WorktreesOverviewModalViewModel>();
vm.Configure(rawId, row.Name);
await vm.LoadAsync();
await ShowWorktreesOverviewModal(vm);
await Dialogs.ShowWorktreesOverviewAsync(vm);
}
finally { _worktreesOverviewOpen = false; }
}
@@ -297,11 +294,11 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
UserLists.Add(item);
SelectedList = item;
if (ShowListSettingsModal is not null && _services is not null)
if (Dialogs is not null && _services is not null)
{
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType);
await ShowListSettingsModal(vm);
await Dialogs.ShowListSettingsAsync(vm);
if (vm.Deleted) await LoadAsync();
else await RefreshRowAsync(item.Id);
}

View File

@@ -41,35 +41,30 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
public Func<MergeModalViewModel> ResolveMergeVm => _mergeVmFactory;
// Layer C seam: composition root sets the factory; MainWindow sets the dialog opener.
// The integrator connects Layer A/B's RequestConflictResolution(taskId, target) to this method.
// Layer C seam: composition root sets the factory; the dialog service shows the resolver.
public Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>? ConflictResolverFactory { get; set; }
public Func<ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel, Task>? ShowConflictResolver { get; set; }
// Single dialog seam (set by MainWindow); propagated to the lists island.
private IDialogService? _dialogs;
public IDialogService? Dialogs
{
get => _dialogs;
set
{
_dialogs = value;
if (Lists is not null) Lists.Dialogs = value;
}
}
public async Task RequestConflictResolutionAsync(string taskId, string targetBranch)
{
if (ConflictResolverFactory is null || ShowConflictResolver is null) return;
if (ConflictResolverFactory is null || Dialogs is null) return;
var vm = ConflictResolverFactory(taskId);
var hasConflicts = await vm.OpenAsync(targetBranch);
if (hasConflicts)
await ShowConflictResolver(vm);
await Dialogs.ShowConflictResolverAsync(vm);
}
// Set by MainWindow to open the About dialog.
public Func<AboutModalViewModel, Task>? ShowAboutModal { get; set; }
// Set by MainWindow to open the repo-import dialog.
public Func<RepoImportModalViewModel, Task>? ShowRepoImportModal { get; set; }
// Set by MainWindow to open the global worktrees overview dialog.
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; }
// Set by MainWindow to open the weekly report dialog.
public Func<WeeklyReportModalViewModel, Task>? ShowWeeklyReportModal { get; set; }
// Set by MainWindow to open the worker-connection help dialog.
public Func<WorkerConnectionModalViewModel, Task>? ShowWorkerConnectionModal { get; set; }
[ObservableProperty] private bool _isUpdateBannerVisible;
[ObservableProperty] private string? _updateBannerLatestVersion;
[ObservableProperty] private string? _inlineUpdateStatus;
@@ -149,11 +144,11 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
private async Task OpenPlanningConflictAsync(string planningTaskId, string subtaskId)
{
if (ConflictResolverFactory is null || ShowConflictResolver is null) return;
if (ConflictResolverFactory is null || Dialogs is null) return;
var vm = ConflictResolverFactory(subtaskId);
var hasConflicts = await vm.OpenForPlanningAsync(planningTaskId, subtaskId);
if (hasConflicts)
await ShowConflictResolver(vm);
await Dialogs.ShowConflictResolverAsync(vm);
}
// For tests only — does NOT wire up events.
@@ -281,7 +276,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
private async Task OpenAbout()
{
var vm = new AboutModalViewModel();
if (ShowAboutModal is not null) await ShowAboutModal(vm);
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
}
private bool _connectionPromptShown;
@@ -297,7 +292,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
private async Task OpenWorkerConnectionHelpAsync()
{
var vm = new WorkerConnectionModalViewModel(_workerLocator, _installerLocator);
if (ShowWorkerConnectionModal is not null) await ShowWorkerConnectionModal(vm);
if (Dialogs is not null) await Dialogs.ShowWorkerConnectionAsync(vm);
}
[RelayCommand]
@@ -306,10 +301,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async Task OpenRepoImport()
{
if (ShowRepoImportModal is null || _repoImportVmFactory is null) return;
if (Dialogs is null || _repoImportVmFactory is null) return;
var vm = _repoImportVmFactory();
await vm.LoadAsync();
await ShowRepoImportModal(vm);
await Dialogs.ShowRepoImportAsync(vm);
if (Lists is not null) await Lists.LoadAsync();
}
@@ -318,14 +313,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async Task OpenWorktreesOverviewGlobalAsync()
{
if (ShowWorktreesOverviewModal is null || _worktreesOverviewOpen) return;
if (Dialogs is null || _worktreesOverviewOpen) return;
_worktreesOverviewOpen = true;
try
{
var vm = _worktreesOverviewVmFactory();
vm.Configure(null, null);
await vm.LoadAsync();
await ShowWorktreesOverviewModal(vm);
await Dialogs.ShowWorktreesOverviewAsync(vm);
}
finally { _worktreesOverviewOpen = false; }
}
@@ -335,13 +330,13 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
[RelayCommand]
private async Task OpenWeeklyReport()
{
if (ShowWeeklyReportModal is null || _weeklyReportOpen) return;
if (Dialogs is null || _weeklyReportOpen) return;
_weeklyReportOpen = true;
try
{
var vm = _weeklyReportVmFactory();
await vm.InitializeAsync();
await ShowWeeklyReportModal(vm);
await Dialogs.ShowWeeklyReportAsync(vm);
}
finally { _weeklyReportOpen = false; }
}