fix(ui): Reactivität — Description-Save-Race, UsageMonitor-Leak, Listen-Live-Refresh
Drei unabhängige Reactivity-Bugs aus dem Polish-Audit 2026-08-20: 1. Description-Autosave überschrieb den falschen Task, weil SaveDescriptionAsync Task.Id/EditableDescription erst nach dem 400ms-Debounce las statt Row+Wert an der Aufrufstelle zu capturen (wie SaveTitleAsync es schon tat). Bind() cancelt jetzt zusätzlich einen laufenden Title-/Description-Save der vorherigen Row. 2. UsageMonitorModalViewModel abonnierte UsageUpdatedEvent erst nach dem Erst-Load-Await — schloss man das Modal währenddessen, lief das Unsubscribe in Close() ins Leere und die VM hing für immer am WorkerClient. Ein _isClosed-Flag wird jetzt nach dem Await geprüft, bevor abonniert wird. 3. Per MCP erstellte Listen blieben unsichtbar: RefreshRowAsync hatte keinen Add-Zweig für unbekannte Ids und verglich zudem die falsche Id-Form (der Worker broadcastet die rohe DB-Id, nie die "user:"-prefixte Row-Id). Ein Reconnect lud zudem nur Counts statt der vollen Listen neu.
This commit is contained in:
@@ -570,20 +570,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
if (_suppressDescSave || Task is null) return;
|
||||
_descSaveCts?.Cancel();
|
||||
_descSaveCts = new CancellationTokenSource();
|
||||
_ = SaveDescriptionAsync(_descSaveCts.Token);
|
||||
// Capture the row so a task switch mid-debounce cannot write A's text into B's description.
|
||||
_ = SaveDescriptionAsync(Task, value, _descSaveCts.Token);
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task SaveDescriptionAsync(CancellationToken ct)
|
||||
private async System.Threading.Tasks.Task SaveDescriptionAsync(
|
||||
TaskRowViewModel row, string value, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await System.Threading.Tasks.Task.Delay(400, ct);
|
||||
if (Task is null) return;
|
||||
await using var ctx = _dbFactory.CreateDbContext();
|
||||
var repo = new TaskRepository(ctx);
|
||||
var entity = await repo.GetByIdAsync(Task.Id);
|
||||
var entity = await repo.GetByIdAsync(row.Id);
|
||||
if (entity is null) return;
|
||||
entity.Description = string.IsNullOrWhiteSpace(EditableDescription) ? null : EditableDescription;
|
||||
entity.Description = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
await repo.UpdateAsync(entity);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
@@ -617,6 +618,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
_loadCts = new CancellationTokenSource();
|
||||
var ct = _loadCts.Token;
|
||||
|
||||
// A pending debounced title/description save targets the row being unbound —
|
||||
// let it be, don't have it land on whichever row replaces it.
|
||||
_titleSaveCts?.Cancel();
|
||||
_descSaveCts?.Cancel();
|
||||
|
||||
Task = row;
|
||||
OnPropertyChanged(nameof(TaskIdBadge));
|
||||
Monitor.Reset();
|
||||
|
||||
Reference in New Issue
Block a user