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:
@@ -805,6 +805,14 @@
|
|||||||
"done": "Fertig"
|
"done": "Fertig"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dialog": {
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"ok": "OK",
|
||||||
|
"confirmTitle": "Bestätigen",
|
||||||
|
"errorTitle": "Fehler"
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"menu": {
|
"menu": {
|
||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
|
|||||||
@@ -805,6 +805,14 @@
|
|||||||
"done": "Done"
|
"done": "Done"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dialog": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"delete": "Delete",
|
||||||
|
"ok": "OK",
|
||||||
|
"confirmTitle": "Confirm",
|
||||||
|
"errorTitle": "Error"
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"menu": {
|
"menu": {
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ public interface IDialogService
|
|||||||
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
||||||
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
||||||
|
|
||||||
/// <summary>Modal yes/no confirmation. Returns true only when confirmed.</summary>
|
/// <summary>Modal yes/no confirmation. Returns true only when confirmed. <paramref name="confirmLabel"/>
|
||||||
Task<bool> ConfirmAsync(string message);
|
/// overrides the default localized "Confirm" button text (e.g. a localized "Delete" for destructive actions).</summary>
|
||||||
|
Task<bool> ConfirmAsync(string message, string? confirmLabel = null);
|
||||||
|
|
||||||
/// <summary>Modal error notice with a single dismiss button.</summary>
|
/// <summary>Modal error notice with a single dismiss button.</summary>
|
||||||
Task ShowErrorAsync(string message);
|
Task ShowErrorAsync(string message);
|
||||||
|
|||||||
@@ -291,11 +291,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
// Set by shell so DeleteTaskCommand can remove from list
|
// Set by shell so DeleteTaskCommand can remove from list
|
||||||
public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }
|
public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }
|
||||||
|
|
||||||
// Set by the view so DeleteTaskCommand can prompt yes/no before deleting
|
// Set by the shell (mirrors ListsIslandViewModel.Dialogs) so confirm/error prompts go
|
||||||
public Func<string, System.Threading.Tasks.Task<bool>>? ConfirmAsync { get; set; }
|
// through the one shared dialog implementation instead of a view-local copy.
|
||||||
|
public IDialogService? Dialogs { get; set; }
|
||||||
// Set by the view so DeleteTaskCommand can show an error message
|
|
||||||
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
|
|
||||||
|
|
||||||
// ── Review ──────────────────────────────────────────────────────────────
|
// ── Review ──────────────────────────────────────────────────────────────
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -1042,9 +1040,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
if (Task == null) return;
|
if (Task == null) return;
|
||||||
var row = Task;
|
var row = Task;
|
||||||
if (ConfirmAsync != null)
|
if (Dialogs != null)
|
||||||
{
|
{
|
||||||
var ok = await ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone.");
|
var ok = await Dialogs.ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone.", Loc.T("dialog.delete"));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
// Routed through the worker (mirrors the MCP delete_task tool) so a deleted child
|
// Routed through the worker (mirrors the MCP delete_task tool) so a deleted child
|
||||||
@@ -1060,14 +1058,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
// Belt and braces: the connection can drop between the CanExecute check and
|
// Belt and braces: the connection can drop between the CanExecute check and
|
||||||
// this call, so a stray throw here must surface, not vanish silently.
|
// this call, so a stray throw here must surface, not vanish silently.
|
||||||
if (ShowErrorAsync != null)
|
if (Dialogs != null)
|
||||||
await ShowErrorAsync(ex.Message);
|
await Dialogs.ShowErrorAsync(ex.Message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!deleted)
|
if (!deleted)
|
||||||
{
|
{
|
||||||
if (ShowErrorAsync != null)
|
if (Dialogs != null)
|
||||||
await ShowErrorAsync(error ?? "Delete failed.");
|
await Dialogs.ShowErrorAsync(error ?? "Delete failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (DeleteFromList != null)
|
if (DeleteFromList != null)
|
||||||
@@ -1216,10 +1214,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
||||||
{
|
{
|
||||||
if (Task == null) return;
|
if (Task == null) return;
|
||||||
if (ConfirmAsync == null) return;
|
if (Dialogs == null) return;
|
||||||
|
|
||||||
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
|
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
|
||||||
var ok = await ConfirmAsync(
|
var ok = await Dialogs.ConfirmAsync(
|
||||||
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
|
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
@@ -1279,31 +1277,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
// Nothing landed -- the target working dir has uncommitted changes (often another
|
// Nothing landed -- the target working dir has uncommitted changes (often another
|
||||||
// session's WIP in the shared main checkout). Offer the one-shot stash retry; git
|
// session's WIP in the shared main checkout). Offer the one-shot stash retry; git
|
||||||
// restores the changes itself once the merge is through.
|
// restores the changes itself once the merge is through.
|
||||||
if (result?.Status == "dirty_target" && ConfirmAsync != null
|
if (result?.Status == "dirty_target" && Dialogs != null
|
||||||
&& await ConfirmAsync(Loc.T("vm.detailsIsland.stashAndMergeConfirm")))
|
&& await Dialogs.ConfirmAsync(Loc.T("vm.detailsIsland.stashAndMergeConfirm")))
|
||||||
{
|
{
|
||||||
ApproveOp.Report(Loc.T("ops.merge.merging"));
|
ApproveOp.Report(Loc.T("ops.merge.merging"));
|
||||||
result = await _worker.ApproveReviewAsync(
|
result = await _worker.ApproveReviewAsync(
|
||||||
taskId, Merge.SelectedMergeTarget ?? "", autostash: true);
|
taskId, Merge.SelectedMergeTarget ?? "", autostash: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.Status == "dirty_target" && ShowErrorAsync != null)
|
if (result?.Status == "dirty_target" && Dialogs != null)
|
||||||
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.dirtyTarget"));
|
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.dirtyTarget"));
|
||||||
else if (!hasChildren && result?.Status == "conflict")
|
else if (!hasChildren && result?.Status == "conflict")
|
||||||
await _merge.ResolveConflictAsync(taskId, Merge.SelectedMergeTarget ?? "");
|
await _merge.ResolveConflictAsync(taskId, Merge.SelectedMergeTarget ?? "");
|
||||||
// The merge itself already landed; the verify command failed, so the task stayed
|
// The merge itself already landed; the verify command failed, so the task stayed
|
||||||
// out of Done. Surface that instead of silently looking like nothing happened.
|
// out of Done. Surface that instead of silently looking like nothing happened.
|
||||||
else if (!hasChildren && result?.Status == "verify_failed" && ShowErrorAsync != null)
|
else if (!hasChildren && result?.Status == "verify_failed" && Dialogs != null)
|
||||||
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
|
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
|
||||||
// Nothing landed -- the merge was refused before touching the repo because the
|
// Nothing landed -- the merge was refused before touching the repo because the
|
||||||
// branch would have overwritten an untracked file in the target working directory.
|
// branch would have overwritten an untracked file in the target working directory.
|
||||||
else if (!hasChildren && result?.Status == "untracked_collision" && ShowErrorAsync != null)
|
else if (!hasChildren && result?.Status == "untracked_collision" && Dialogs != null)
|
||||||
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.untrackedCollision"));
|
await Dialogs.ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.untrackedCollision"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (ShowErrorAsync != null)
|
if (Dialogs != null)
|
||||||
await ShowErrorAsync(ex.Message);
|
await Dialogs.ShowErrorAsync(ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1348,7 +1346,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
try { await _worker.SubmitTaskForReviewAsync(Task.Id); }
|
try { await _worker.SubmitTaskForReviewAsync(Task.Id); }
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
|
if (Dialogs != null) await Dialogs.ShowErrorAsync(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,9 +1377,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async System.Threading.Tasks.Task ResetReviewAsync()
|
private async System.Threading.Tasks.Task ResetReviewAsync()
|
||||||
{
|
{
|
||||||
if (Task is null || !_worker.IsConnected || ConfirmAsync is null) return;
|
if (Task is null || !_worker.IsConnected || Dialogs is null) return;
|
||||||
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
|
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
|
||||||
var ok = await ConfirmAsync(
|
var ok = await Dialogs.ConfirmAsync(
|
||||||
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
|
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try { await _worker.ResetTaskAsync(Task.Id); }
|
try { await _worker.ResetTaskAsync(Task.Id); }
|
||||||
|
|||||||
@@ -1209,9 +1209,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
// block removing a completed planning parent together with its done children.
|
// block removing a completed planning parent together with its done children.
|
||||||
var toDelete = completed.OrderByDescending(r => r.IsChild).ToList();
|
var toDelete = completed.OrderByDescending(r => r.IsChild).ToList();
|
||||||
|
|
||||||
if (ConfirmAsync is not null)
|
if (Dialogs is not null)
|
||||||
{
|
{
|
||||||
var ok = await ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone.");
|
var ok = await Dialogs.ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone.", Loc.T("dialog.delete"));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,9 +1339,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private async Task DeleteTaskAsync(TaskRowViewModel? row)
|
private async Task DeleteTaskAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || row.HasInteractiveSession || _worker is null) return;
|
if (row is null || row.HasInteractiveSession || _worker is null) return;
|
||||||
if (ConfirmAsync is not null)
|
if (Dialogs is not null)
|
||||||
{
|
{
|
||||||
var ok = await ConfirmAsync(Loc.T("vm.tasksIsland.deleteTaskConfirm", row.Title));
|
var ok = await Dialogs.ConfirmAsync(Loc.T("vm.tasksIsland.deleteTaskConfirm", row.Title), Loc.T("dialog.delete"));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1653,8 +1653,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
if (outcome.Result == DiscardPlanningResult.BlockedByQueuedChildren)
|
if (outcome.Result == DiscardPlanningResult.BlockedByQueuedChildren)
|
||||||
{
|
{
|
||||||
if (ConfirmAsync is null) return;
|
if (Dialogs is null) return;
|
||||||
var ok = await ConfirmAsync(
|
var ok = await Dialogs.ConfirmAsync(
|
||||||
$"{outcome.QueuedChildrenCount} child task(s) are queued.\n" +
|
$"{outcome.QueuedChildrenCount} child task(s) are queued.\n" +
|
||||||
"Dequeue them and discard the planning session?");
|
"Dequeue them and discard the planning session?");
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -1663,18 +1663,13 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
else if (outcome.Result == DiscardPlanningResult.BlockedByRunningChildren)
|
else if (outcome.Result == DiscardPlanningResult.BlockedByRunningChildren)
|
||||||
{
|
{
|
||||||
if (ConfirmAsync is null) return;
|
if (Dialogs is null) return;
|
||||||
await ConfirmAsync(
|
await Dialogs.ConfirmAsync(
|
||||||
$"{outcome.RunningChildrenCount} child task(s) are still running.\n" +
|
$"{outcome.RunningChildrenCount} child task(s) are still running.\n" +
|
||||||
"Cancel them first, then try again.");
|
"Cancel them first, then try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wired by the view via <see cref="ShowConfirmAsync"/>. Returns true when the user confirms.
|
|
||||||
/// </summary>
|
|
||||||
public Func<string, Task<bool>>? ConfirmAsync { get; set; }
|
|
||||||
|
|
||||||
private bool CanRunPlanningOperation() => !PlanningOperation.IsRunning;
|
private bool CanRunPlanningOperation() => !PlanningOperation.IsRunning;
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanRunPlanningOperation))]
|
[RelayCommand(CanExecute = nameof(CanRunPlanningOperation))]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
// Set by MainWindow so a reveal can bring the main window to the foreground.
|
// Set by MainWindow so a reveal can bring the main window to the foreground.
|
||||||
public Action? BringToFront { get; set; }
|
public Action? BringToFront { get; set; }
|
||||||
|
|
||||||
// Single dialog seam (set by MainWindow); propagated to the lists island.
|
// Single dialog seam (set by MainWindow); propagated to the islands.
|
||||||
private IDialogService? _dialogs;
|
private IDialogService? _dialogs;
|
||||||
public IDialogService? Dialogs
|
public IDialogService? Dialogs
|
||||||
{
|
{
|
||||||
@@ -78,6 +78,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
_dialogs = value;
|
_dialogs = value;
|
||||||
if (Lists is not null) Lists.Dialogs = value;
|
if (Lists is not null) Lists.Dialogs = value;
|
||||||
if (Tasks is not null) Tasks.Dialogs = value;
|
if (Tasks is not null) Tasks.Dialogs = value;
|
||||||
|
if (Details is not null) Details.Dialogs = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,6 @@ public partial class DetailsIslandView : UserControl
|
|||||||
await modal.ShowDialog(owner);
|
await modal.ShowDialog(owner);
|
||||||
};
|
};
|
||||||
|
|
||||||
vm.ConfirmAsync = ShowConfirmAsync;
|
|
||||||
vm.ShowErrorAsync = ShowErrorDialogAsync;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,87 +211,4 @@ public partial class DetailsIslandView : UserControl
|
|||||||
_descriptionHeights[id] = DetailBodyGrid.RowDefinitions[0].Height.Value;
|
_descriptionHeights[id] = DetailBodyGrid.RowDefinitions[0].Height.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message)
|
|
||||||
{
|
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
||||||
if (owner == null) return;
|
|
||||||
|
|
||||||
var ok = new Button { Content = "OK", MinWidth = 90 };
|
|
||||||
|
|
||||||
var dialog = new Window
|
|
||||||
{
|
|
||||||
Title = "Error",
|
|
||||||
Width = 360,
|
|
||||||
SizeToContent = SizeToContent.Height,
|
|
||||||
CanResize = false,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
ShowInTaskbar = false,
|
|
||||||
Background = this.FindResource("SurfaceBrush") as IBrush,
|
|
||||||
Content = new StackPanel
|
|
||||||
{
|
|
||||||
Spacing = 16,
|
|
||||||
Margin = new Thickness(20),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
|
||||||
new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
Spacing = 8,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
Children = { ok }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ok.Click += (_, _) => dialog.Close();
|
|
||||||
|
|
||||||
await dialog.ShowDialog(owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task<bool> ShowConfirmAsync(string message)
|
|
||||||
{
|
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
||||||
if (owner == null) return false;
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
var cancel = new Button { Content = "Cancel", MinWidth = 90 };
|
|
||||||
var confirm = new Button { Content = "Delete", MinWidth = 90, Classes = { "danger" } };
|
|
||||||
|
|
||||||
var dialog = new Window
|
|
||||||
{
|
|
||||||
Title = "Confirm",
|
|
||||||
Width = 360,
|
|
||||||
SizeToContent = SizeToContent.Height,
|
|
||||||
CanResize = false,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
ShowInTaskbar = false,
|
|
||||||
Background = this.FindResource("SurfaceBrush") as IBrush,
|
|
||||||
Content = new StackPanel
|
|
||||||
{
|
|
||||||
Spacing = 16,
|
|
||||||
Margin = new Thickness(20),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
|
||||||
new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
Spacing = 8,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
Children = { cancel, confirm }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cancel.Click += (_, _) => { tcs.TrySetResult(false); dialog.Close(); };
|
|
||||||
confirm.Click += (_, _) => { tcs.TrySetResult(true); dialog.Close(); };
|
|
||||||
dialog.Closed += (_, _) => tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
_ = dialog.ShowDialog(owner);
|
|
||||||
return await tcs.Task;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ public partial class TasksIslandView : UserControl
|
|||||||
await modal.ShowDialog(owner);
|
await modal.ShowDialog(owner);
|
||||||
// ShowDialog completes once the window is closed (CloseAction or OS close).
|
// ShowDialog completes once the window is closed (CloseAction or OS close).
|
||||||
};
|
};
|
||||||
vm.ConfirmAsync = ShowConfirmAsync;
|
|
||||||
vm.SelectionChanged += (_, _) => ScrollSelectedIntoView();
|
vm.SelectionChanged += (_, _) => ScrollSelectedIntoView();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -71,50 +70,6 @@ public partial class TasksIslandView : UserControl
|
|||||||
Dispatcher.UIThread.Post(() => RowsListBox.ScrollIntoView(target), DispatcherPriority.Background);
|
Dispatcher.UIThread.Post(() => RowsListBox.ScrollIntoView(target), DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task<bool> ShowConfirmAsync(string message)
|
|
||||||
{
|
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
||||||
if (owner is null) return false;
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
var cancel = new Button { Content = "Cancel", MinWidth = 90 };
|
|
||||||
var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
|
|
||||||
|
|
||||||
var dialog = new Window
|
|
||||||
{
|
|
||||||
Title = "Confirm",
|
|
||||||
Width = 380,
|
|
||||||
SizeToContent = SizeToContent.Height,
|
|
||||||
CanResize = false,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
ShowInTaskbar = false,
|
|
||||||
Background = this.FindResource("SurfaceBrush") as IBrush,
|
|
||||||
Content = new StackPanel
|
|
||||||
{
|
|
||||||
Margin = new Thickness(20),
|
|
||||||
Spacing = 16,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
|
||||||
new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
Spacing = 8,
|
|
||||||
Children = { cancel, confirm },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
cancel.Click += (_, _) => { tcs.TrySetResult(false); dialog.Close(); };
|
|
||||||
confirm.Click += (_, _) => { tcs.TrySetResult(true); dialog.Close(); };
|
|
||||||
dialog.Closed += (_, _) => tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
_ = dialog.ShowDialog(owner);
|
|
||||||
return await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Custom ghost drag ────────────────────────────────────────────────────
|
// ── Custom ghost drag ────────────────────────────────────────────────────
|
||||||
// Replaces both the OLE DoDragDropAsync reorder and the OLE drop-to-queue path: a hand-built
|
// Replaces both the OLE DoDragDropAsync reorder and the OLE drop-to-queue path: a hand-built
|
||||||
// drag (pointer capture + a transparent topmost ghost window) is the only way to get a
|
// drag (pointer capture + a transparent topmost ghost window) is the only way to get a
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Avalonia.Layout;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
namespace ClaudeDo.Ui.Views.MissionControl;
|
||||||
@@ -73,20 +74,21 @@ public partial class MissionControlView : UserControl
|
|||||||
UpdateQueueColumn();
|
UpdateQueueColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abandon-run confirmation -- same TaskCompletionSource-backed plain Window as
|
// Abandon-run confirmation -- own plain-Window TaskCompletionSource dialog, not routed
|
||||||
// TasksIslandView.ShowConfirmAsync (no shared control for this shape yet).
|
// through WindowDialogService: it's owned by the main window, so showing it while Mission
|
||||||
|
// Control (a separate Window) is focused would put it behind Mission Control.
|
||||||
private async Task<bool> ShowConfirmAsync(string message)
|
private async Task<bool> ShowConfirmAsync(string message)
|
||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner is null) return false;
|
if (owner is null) return false;
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
var cancel = new Button { Content = "Cancel", MinWidth = 90 };
|
var cancel = new Button { Content = Loc.T("dialog.cancel"), MinWidth = 90 };
|
||||||
var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
|
var confirm = new Button { Content = Loc.T("dialog.confirm"), MinWidth = 90, Classes = { "danger" } };
|
||||||
|
|
||||||
var dialog = new Window
|
var dialog = new Window
|
||||||
{
|
{
|
||||||
Title = "Confirm",
|
Title = Loc.T("dialog.confirmTitle"),
|
||||||
Width = 380,
|
Width = 380,
|
||||||
SizeToContent = SizeToContent.Height,
|
SizeToContent = SizeToContent.Height,
|
||||||
CanResize = false,
|
CanResize = false,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
@@ -71,7 +72,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
|
|
||||||
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
||||||
{
|
{
|
||||||
vm.Worktrees.ConfirmAsync = ConfirmAsync;
|
vm.Worktrees.ConfirmAsync = message => ConfirmAsync(message);
|
||||||
var dlg = new SettingsModalView { DataContext = vm };
|
var dlg = new SettingsModalView { DataContext = vm };
|
||||||
vm.RequestRepoImport = () =>
|
vm.RequestRepoImport = () =>
|
||||||
{
|
{
|
||||||
@@ -90,7 +91,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
{
|
{
|
||||||
var dlg = new ListSettingsModalView { DataContext = vm };
|
var dlg = new ListSettingsModalView { DataContext = vm };
|
||||||
vm.CloseAction = () => dlg.Close();
|
vm.CloseAction = () => dlg.Close();
|
||||||
vm.ConfirmAsync = ConfirmAsync;
|
vm.ConfirmAsync = message => ConfirmAsync(message);
|
||||||
vm.ShowErrorAsync = ShowErrorAsync;
|
vm.ShowErrorAsync = ShowErrorAsync;
|
||||||
await dlg.ShowDialog(_owner);
|
await dlg.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
_diffViewerWindow = diffDlg;
|
_diffViewerWindow = diffDlg;
|
||||||
diffDlg.Show(_owner);
|
diffDlg.Show(_owner);
|
||||||
};
|
};
|
||||||
vm.ConfirmAction = ConfirmAsync;
|
vm.ConfirmAction = message => ConfirmAsync(message);
|
||||||
if (Shell is { } shell)
|
if (Shell is { } shell)
|
||||||
{
|
{
|
||||||
vm.ResolveMergeVm = shell.ResolveMergeVm;
|
vm.ResolveMergeVm = shell.ResolveMergeVm;
|
||||||
@@ -206,13 +207,13 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
_cheatSheets.Activate();
|
_cheatSheets.Activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> ConfirmAsync(string message)
|
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
var cancel = new Button { Content = "Cancel", MinWidth = 90 };
|
var cancel = new Button { Content = Loc.T("dialog.cancel"), MinWidth = 90 };
|
||||||
var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
|
var confirm = new Button { Content = confirmLabel ?? Loc.T("dialog.confirm"), MinWidth = 90, Classes = { "danger" } };
|
||||||
|
|
||||||
var dialog = NoticeWindow("Confirm", 380, message,
|
var dialog = NoticeWindow(Loc.T("dialog.confirmTitle"), 380, message,
|
||||||
new StackPanel
|
new StackPanel
|
||||||
{
|
{
|
||||||
Orientation = Orientation.Horizontal,
|
Orientation = Orientation.Horizontal,
|
||||||
@@ -231,8 +232,8 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
|
|
||||||
public Task ShowErrorAsync(string message)
|
public Task ShowErrorAsync(string message)
|
||||||
{
|
{
|
||||||
var ok = new Button { Content = "OK", MinWidth = 90 };
|
var ok = new Button { Content = Loc.T("dialog.ok"), MinWidth = 90 };
|
||||||
var dialog = NoticeWindow("Error", 360, message,
|
var dialog = NoticeWindow(Loc.T("dialog.errorTitle"), 360, message,
|
||||||
new StackPanel
|
new StackPanel
|
||||||
{
|
{
|
||||||
Orientation = Orientation.Horizontal,
|
Orientation = Orientation.Horizontal,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class IslandsShellViewModelShutdownTests
|
|||||||
|
|
||||||
private sealed class FakeDialogService(bool confirmResult) : IDialogService
|
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 ShowAboutAsync(AboutModalViewModel vm) => throw new NotImplementedException();
|
||||||
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
|
public Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm) => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.Services.Interfaces;
|
using ClaudeDo.Ui.Services.Interfaces;
|
||||||
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Tests;
|
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>
|
/// <summary><see cref="StubWorkerClient"/> is abstract — this is the concrete no-op subclass.</summary>
|
||||||
public sealed class DefaultStub : StubWorkerClient { }
|
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; };
|
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
|
||||||
var closed = false;
|
var closed = false;
|
||||||
vm.CloseDetail = () => closed = true;
|
vm.CloseDetail = () => closed = true;
|
||||||
string? reportedError = null;
|
var dialogs = new RecordingDialogService();
|
||||||
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
|
vm.Dialogs = dialogs;
|
||||||
|
|
||||||
await vm.DeleteTaskCommand.ExecuteAsync(null);
|
await vm.DeleteTaskCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
Assert.Equal(fkMessage, reportedError);
|
Assert.Equal(fkMessage, dialogs.LastErrorMessage);
|
||||||
Assert.False(deleteFromListCalled);
|
Assert.False(deleteFromListCalled);
|
||||||
Assert.False(closed);
|
Assert.False(closed);
|
||||||
}
|
}
|
||||||
@@ -136,12 +136,12 @@ public class DetailsIslandDeleteTaskTests : IDisposable
|
|||||||
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
|
vm.DeleteFromList = _ => { deleteFromListCalled = true; return Task.CompletedTask; };
|
||||||
var closed = false;
|
var closed = false;
|
||||||
vm.CloseDetail = () => closed = true;
|
vm.CloseDetail = () => closed = true;
|
||||||
string? reportedError = null;
|
var dialogs = new RecordingDialogService();
|
||||||
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
|
vm.Dialogs = dialogs;
|
||||||
|
|
||||||
await vm.DeleteTaskCommand.ExecuteAsync(null);
|
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(deleteFromListCalled);
|
||||||
Assert.False(closed);
|
Assert.False(closed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ public class DetailsIslandErrorFeedbackTests : IDisposable
|
|||||||
var vm = BuildVm(worker);
|
var vm = BuildVm(worker);
|
||||||
vm.Bind(new TaskRowViewModel { Id = "task-reset-throw", Status = TaskStatus.Failed });
|
vm.Bind(new TaskRowViewModel { Id = "task-reset-throw", Status = TaskStatus.Failed });
|
||||||
vm.Monitor.ApplyState(TaskStatus.Failed);
|
vm.Monitor.ApplyState(TaskStatus.Failed);
|
||||||
vm.ConfirmAsync = _ => Task.FromResult(true);
|
vm.Dialogs = new RecordingDialogService { ConfirmResult = true };
|
||||||
|
|
||||||
string? reportedError = null;
|
string? reportedError = null;
|
||||||
vm.ErrorReported += msg => reportedError = msg;
|
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.Bind(new TaskRowViewModel { Id = "task-err-1", Status = TaskStatus.WaitingForReview });
|
||||||
vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
|
vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
|
||||||
|
|
||||||
string? reportedError = null;
|
var dialogs = new RecordingDialogService();
|
||||||
vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };
|
vm.Dialogs = dialogs;
|
||||||
|
|
||||||
await vm.ApproveReviewCommand.ExecuteAsync(null);
|
await vm.ApproveReviewCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
Assert.Equal(worker.ExceptionMessage, reportedError);
|
Assert.Equal(worker.ExceptionMessage, dialogs.LastErrorMessage);
|
||||||
Assert.False(vm.ApproveOp.IsRunning);
|
Assert.False(vm.ApproveOp.IsRunning);
|
||||||
Assert.True(vm.ApproveReviewCommand.CanExecute(null));
|
Assert.True(vm.ApproveReviewCommand.CanExecute(null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class ListsIslandCreateListCancelTests : IDisposable
|
|||||||
public Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) => throw new NotImplementedException();
|
public Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm) => throw new NotImplementedException();
|
||||||
public Task ShowConflictResolverAsync(ConflictResolverViewModel vm) => throw new NotImplementedException();
|
public Task ShowConflictResolverAsync(ConflictResolverViewModel vm) => throw new NotImplementedException();
|
||||||
public Task ShowLogVisualizerAsync(LogVisualizerViewModel 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 Task ShowErrorAsync(string message) => Task.CompletedTask;
|
||||||
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
||||||
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
|
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class TasksIslandMoveToListTests : IDisposable
|
|||||||
public int ConfirmCalls;
|
public int ConfirmCalls;
|
||||||
public string? LastMessage;
|
public string? LastMessage;
|
||||||
|
|
||||||
public Task<bool> ConfirmAsync(string message)
|
public Task<bool> ConfirmAsync(string message, string? confirmLabel = null)
|
||||||
{
|
{
|
||||||
ConfirmCalls++;
|
ConfirmCalls++;
|
||||||
LastMessage = message;
|
LastMessage = message;
|
||||||
|
|||||||
Reference in New Issue
Block a user