fix(ui): duplizierter Confirm-Dialog mit "Delete"-Knopf – zusammenfuehren und lokalisieren

F4: DetailsIslandView/TasksIslandView hatten je eine view-lokale Confirm/Error-Dialog-Kopie,
teils mit hartkodiertem "Delete"-Knopf fuer jede Aktion (inkl. der Stash+Merge-Bestaetigung,
die nichts loescht). Beide Kopien entfernt; DetailsIslandViewModel/TasksIslandViewModel routen
Confirm/Error jetzt durch den geteilten Dialogs-Seam (IDialogService, propagiert vom Shell wie
schon bei Lists/Tasks) statt ueber eine vom Code-behind gesetzte Func-Property.

IDialogService.ConfirmAsync bekommt einen optionalen confirmLabel-Parameter (Default: lokalisiertes
"Confirm"); nur die echten Delete-Aktionen (Task loeschen, abgeschlossene Tasks leeren) uebergeben
das lokalisierte "Delete"-Label.

F5: "Cancel"/"Confirm"/"Delete"/"OK" und die Titel "Confirm"/"Error" durch Loc.T mit neuen Keys
unter "dialog.*" ersetzt (en.json + de.json). Mission Controls eigene Dialog-Kopie bleibt bestehen
(eigenes Fenster, WindowDialogService ist an den Haupt-Owner gebunden) — nur ihre Labels wurden
lokalisiert.

Out of scope (siehe Task-Beschreibung): die Nachrichtentexte der Dialoge bleiben unlokalisiert.
This commit is contained in:
Mika Kuns
2026-09-16 15:43:38 +02:00
committed by mika kuns
parent 684b0115e5
commit 86cc309ba9
17 changed files with 126 additions and 199 deletions
@@ -44,7 +44,7 @@ public class IslandsShellViewModelShutdownTests
private sealed class FakeDialogService(bool confirmResult) : IDialogService
{
public Task<bool> ConfirmAsync(string message) => Task.FromResult(confirmResult);
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null) => Task.FromResult(confirmResult);
public Task ShowAboutAsync(AboutModalViewModel vm) => throw new NotImplementedException();
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
+43
View File
@@ -1,6 +1,9 @@
using ClaudeDo.Data;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.Tests;
@@ -31,3 +34,43 @@ public sealed class StubNotesApi : INotesApi
/// <summary><see cref="StubWorkerClient"/> is abstract — this is the concrete no-op subclass.</summary>
public sealed class DefaultStub : StubWorkerClient { }
/// <summary>
/// Records the message/label of the last Confirm/Error call and returns a configurable
/// Confirm result. Covers the ViewModels (DetailsIsland, TasksIsland, ...) that take their
/// dialog seam via a settable <c>Dialogs</c> property instead of a per-call constructor arg.
/// </summary>
public sealed class RecordingDialogService : IDialogService
{
public bool ConfirmResult { get; set; } = true;
public string? LastConfirmMessage { get; private set; }
public string? LastConfirmLabel { get; private set; }
public string? LastErrorMessage { get; private set; }
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null)
{
LastConfirmMessage = message;
LastConfirmLabel = confirmLabel;
return Task.FromResult(ConfirmResult);
}
public Task ShowErrorAsync(string message)
{
LastErrorMessage = message;
return Task.CompletedTask;
}
public Task ShowAboutAsync(AboutModalViewModel vm) => throw new NotImplementedException();
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
public Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm) => throw new NotImplementedException();
public Task ShowSettingsAsync(SettingsModalViewModel vm) => throw new NotImplementedException();
public Task ShowListSettingsAsync(ListSettingsModalViewModel vm) => throw new NotImplementedException();
public Task ShowRepoImportAsync(RepoImportModalViewModel vm) => throw new NotImplementedException();
public Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm) => throw new NotImplementedException();
public Task<IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm) => throw new NotImplementedException();
public Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) => throw new NotImplementedException();
public Task ShowConflictResolverAsync(ConflictResolverViewModel vm) => throw new NotImplementedException();
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
}
@@ -90,12 +90,12 @@ public class DetailsIslandDeleteTaskTests : IDisposable
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
var closed = false;
vm.CloseDetail = () => closed = true;
string? reportedError = null;
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
var dialogs = new RecordingDialogService();
vm.Dialogs = dialogs;
await vm.DeleteTaskCommand.ExecuteAsync(null);
Assert.Equal(fkMessage, reportedError);
Assert.Equal(fkMessage, dialogs.LastErrorMessage);
Assert.False(deleteFromListCalled);
Assert.False(closed);
}
@@ -136,12 +136,12 @@ public class DetailsIslandDeleteTaskTests : IDisposable
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
var closed = false;
vm.CloseDetail = () => closed = true;
string? reportedError = null;
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
var dialogs = new RecordingDialogService();
vm.Dialogs = dialogs;
await vm.DeleteTaskCommand.ExecuteAsync(null);
Assert.Equal("Connection is not active.", reportedError);
Assert.Equal("Connection is not active.", dialogs.LastErrorMessage);
Assert.False(deleteFromListCalled);
Assert.False(closed);
}
@@ -164,7 +164,7 @@ public class DetailsIslandErrorFeedbackTests : IDisposable
var vm = BuildVm(worker);
vm.Bind(new TaskRowViewModel { Id = "task-reset-throw", Status = TaskStatus.Failed });
vm.Monitor.ApplyState(TaskStatus.Failed);
vm.ConfirmAsync = _ => Task.FromResult(true);
vm.Dialogs = new RecordingDialogService { ConfirmResult = true };
string? reportedError = null;
vm.ErrorReported += msg => reportedError = msg;
@@ -153,12 +153,12 @@ public class DetailsIslandReviewActionsTests : IDisposable
vm.Bind(new TaskRowViewModel { Id = "task-err-1", Status = TaskStatus.WaitingForReview });
vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
string? reportedError = null;
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
var dialogs = new RecordingDialogService();
vm.Dialogs = dialogs;
await vm.ApproveReviewCommand.ExecuteAsync(null);
Assert.Equal(worker.ExceptionMessage, reportedError);
Assert.Equal(worker.ExceptionMessage, dialogs.LastErrorMessage);
Assert.False(vm.ApproveOp.IsRunning);
Assert.True(vm.ApproveReviewCommand.CanExecute(null));
}
@@ -62,7 +62,7 @@ public class ListsIslandCreateListCancelTests : IDisposable
public Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) => throw new NotImplementedException();
public Task ShowConflictResolverAsync(ConflictResolverViewModel vm) => throw new NotImplementedException();
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
public Task<bool> ConfirmAsync(string message) => Task.FromResult(true);
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null) => Task.FromResult(true);
public Task ShowErrorAsync(string message) => Task.CompletedTask;
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
@@ -43,7 +43,7 @@ public class TasksIslandMoveToListTests : IDisposable
public int ConfirmCalls;
public string? LastMessage;
public Task<bool> ConfirmAsync(string message)
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null)
{
ConfirmCalls++;
LastMessage = message;