fix(ui): Ordner-Toggle in der Sidebar schluckt schnelle Klicks

ToggleFolderAsync war ein async [RelayCommand]; AsyncRelayCommand sperrt
sich waehrend des DB-Writes selbst, schnelle Klicks gingen verloren
(Befund F1, docs/test-session-2026-09-16.md).

Variante A: Toggle ist jetzt synchron (reiner UI-State), Persistierung
fire-and-forget mit einem CancellationTokenSource pro Ordner, damit ein
zweiter Klick vor dem ersten Schreibvorgang diesen abbricht - die DB
landet immer beim zuletzt sichtbaren Zustand, nie bei einem veralteten.
Gleiches Muster wie SaveTitleAsync/SaveDescriptionAsync in
DetailsIslandViewModel.

Gegenprueft: RemoveFromFolderAsync/CreateListAsync haben denselben
async-RelayCommand-Schnitt, sind aber Einzelklick-Aktionen (Kontextmenue/
Button), kein Rapid-Click-Ziel wie der Ordner-Toggle - bewusst nicht
umgebaut. Die Modal-oeffnenden Commands sind unkritisch, ein Modal
blockiert ohnehin.
This commit is contained in:
Mika Kuns
2026-09-16 15:43:37 +02:00
committed by mika kuns
parent 7452f6cfe4
commit 5cdfb14ff1
2 changed files with 46 additions and 3 deletions
@@ -263,7 +263,8 @@ public class ListsIslandFolderTests : IDisposable
var folder = vm.Folders[0];
Assert.False(folder.IsCollapsed);
await vm.ToggleFolderCommand.ExecuteAsync(folder);
vm.ToggleFolderCommand.Execute(folder);
await Task.Delay(100);
Assert.True(folder.IsCollapsed);
await using var db = NewContext();
@@ -275,6 +276,31 @@ public class ListsIslandFolderTests : IDisposable
Assert.True(reloaded.Folders[0].IsCollapsed);
}
[Fact]
public async Task ToggleFolder_rapid_double_click_both_toggles_land()
{
// Bug F1 (docs/test-session-2026-09-16.md): the command used to be an async
// [RelayCommand], so AsyncRelayCommand's own CanExecute lock swallowed the second click
// while the first one's DB write was still in flight. Firing two toggles back-to-back
// (no await between them) reproduces exactly that window.
await SeedAsync("f1", ("inside", "Inside", 0));
var vm = NewVm();
await vm.LoadAsync();
var folder = vm.Folders[0];
Assert.False(folder.IsCollapsed);
vm.ToggleFolderCommand.Execute(folder);
vm.ToggleFolderCommand.Execute(folder);
await Task.Delay(100);
// Two flips land back on the original (visible) state...
Assert.False(folder.IsCollapsed);
// ...and the DB ends up agreeing with it, not with the stale mid-flight value.
await using var db = NewContext();
Assert.False((await db.ListFolders.AsNoTracking().FirstAsync(f => f.Id == "f1")).IsCollapsed);
}
[Fact]
public async Task RemoveFromFolder_command_is_the_context_menu_route_out()
{