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

@@ -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; }
}