feat(ui): add delete-list button to List Settings modal

This commit is contained in:
mika kuns
2026-05-29 16:09:17 +02:00
parent 3af8fb9aa0
commit 128fb7d4d2
5 changed files with 98 additions and 10 deletions

View File

@@ -17,7 +17,7 @@ MVVM with CommunityToolkit.Mvvm source generators:
- **TaskEditorView** — Modal dialog for task create/edit - **TaskEditorView** — Modal dialog for task create/edit
- **ListEditorView** — Modal dialog for list create/edit - **ListEditorView** — Modal dialog for list create/edit
- **StatusBarView** — Connection status indicator, active task display - **StatusBarView** — Connection status indicator, active task display
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath. Opened via context menu or gear button on a list row. - **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath; also deletes the list (and its tasks) via a confirmed "Delete list" button. Opened via context menu or gear button on a list row.
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)". - **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running. - **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running.

View File

@@ -48,7 +48,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
var vm = _services.GetRequiredService<ListSettingsModalViewModel>(); var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType); await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType);
await ShowListSettingsModal(vm); await ShowListSettingsModal(vm);
await RefreshRowAsync(row.Id); if (vm.Deleted) await LoadAsync();
else await RefreshRowAsync(row.Id);
} }
[RelayCommand] [RelayCommand]
@@ -225,7 +226,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
var vm = _services.GetRequiredService<ListSettingsModalViewModel>(); var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType); await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType);
await ShowListSettingsModal(vm); await ShowListSettingsModal(vm);
await RefreshRowAsync(item.Id); if (vm.Deleted) await LoadAsync();
else await RefreshRowAsync(item.Id);
} }
} }

View File

@@ -1,17 +1,28 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Modals; namespace ClaudeDo.Ui.ViewModels.Modals;
public sealed partial class ListSettingsModalViewModel : ViewModelBase public sealed partial class ListSettingsModalViewModel : ViewModelBase
{ {
private readonly WorkerClient _worker; private readonly WorkerClient _worker;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
public string ListId { get; set; } = ""; public string ListId { get; set; } = "";
// True after the list was deleted, so the caller reloads the list nav instead of refreshing the row.
public bool Deleted { get; private set; }
// Wired by the view to prompt yes/no before deleting and to surface a blocking-FK error.
public Func<string, Task<bool>>? ConfirmAsync { get; set; }
public Func<string, Task>? ShowErrorAsync { get; set; }
[ObservableProperty] private string _name = ""; [ObservableProperty] private string _name = "";
[ObservableProperty] private string _workingDir = ""; [ObservableProperty] private string _workingDir = "";
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType; [ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
@@ -29,9 +40,10 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
public Action? CloseAction { get; set; } public Action? CloseAction { get; set; }
public ListSettingsModalViewModel(WorkerClient worker) public ListSettingsModalViewModel(WorkerClient worker, IDbContextFactory<ClaudeDoDbContext> dbFactory)
{ {
_worker = worker; _worker = worker;
_dbFactory = dbFactory;
} }
public async Task LoadAsync( public async Task LoadAsync(
@@ -78,6 +90,35 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
CloseAction?.Invoke(); CloseAction?.Invoke();
} }
[RelayCommand]
private async Task DeleteAsync()
{
var displayName = string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name;
if (ConfirmAsync is not null)
{
var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone.");
if (!ok) return;
}
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var lists = new ListRepository(ctx);
await lists.DeleteAsync(ListId);
}
catch (Exception ex) when (
ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)
|| 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.");
return;
}
Deleted = true;
CloseAction?.Invoke();
}
[RelayCommand] [RelayCommand]
private void Cancel() => CloseAction?.Invoke(); private void Cancel() => CloseAction?.Invoke();

View File

@@ -26,6 +26,8 @@ public partial class ListsIslandView : UserControl
{ {
var window = new ListSettingsModalView { DataContext = modal }; var window = new ListSettingsModalView { DataContext = modal };
modal.CloseAction = () => window.Close(); modal.CloseAction = () => window.Close();
modal.ConfirmAsync = ShowConfirmAsync;
modal.ShowErrorAsync = ShowErrorDialogAsync;
var top = TopLevel.GetTopLevel(this) as Window; var top = TopLevel.GetTopLevel(this) as Window;
if (top is null) window.Show(); if (top is null) window.Show();
else await window.ShowDialog(top); else await window.ShowDialog(top);
@@ -93,6 +95,43 @@ public partial class ListsIslandView : UserControl
private static System.Threading.Tasks.Task JumpToTaskAsync(IslandsShellViewModel s, string listId, string taskId) private static System.Threading.Tasks.Task JumpToTaskAsync(IslandsShellViewModel s, string listId, string taskId)
=> JumpToTaskHelper.SelectAsync(s, listId, taskId); => JumpToTaskHelper.SelectAsync(s, listId, taskId);
private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is 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) private async System.Threading.Tasks.Task<bool> ShowConfirmAsync(string message)
{ {
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;

View File

@@ -42,6 +42,10 @@
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/> <Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
</Style> </Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource BloodBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</Window.Styles> </Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}" <Border Background="{DynamicResource SurfaceBrush}"
@@ -168,12 +172,14 @@
Background="{DynamicResource DeepBrush}" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"> BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8" <Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center" Margin="16,0">
HorizontalAlignment="Right" VerticalAlignment="Center" <Button Grid.Column="0" Content="Delete list" Classes="danger"
Margin="16,0"> Command="{Binding DeleteCommand}" MinWidth="90"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/> <Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/> <Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
</StackPanel> </StackPanel>
</Grid>
</Border> </Border>
</Grid> </Grid>