fix(ui): persist title edits from the detail pane

EditableTitle had no save handler at all (only EditableDescription did), so
retitling a task in the details island was silently discarded on rebind. Mirror
the debounced description save, capturing the row so a task switch mid-debounce
cannot retitle the wrong task.
This commit is contained in:
Mika Kuns
2026-07-27 15:02:50 +02:00
parent 62fc5aaa5a
commit edd2774d86
@@ -258,6 +258,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private bool _suppressDescSave; private bool _suppressDescSave;
private CancellationTokenSource? _descSaveCts; private CancellationTokenSource? _descSaveCts;
private bool _suppressTitleSave;
private CancellationTokenSource? _titleSaveCts;
// Set by shell so CloseDetailCommand can clear SelectedTask // Set by shell so CloseDetailCommand can clear SelectedTask
public Action? CloseDetail { get; set; } public Action? CloseDetail { get; set; }
@@ -430,6 +433,39 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
} }
} }
partial void OnEditableTitleChanged(string value)
{
if (_suppressTitleSave || Task is null) return;
_titleSaveCts?.Cancel();
_titleSaveCts = new CancellationTokenSource();
// Capture the row so a task switch mid-debounce cannot retitle the new task.
_ = SaveTitleAsync(Task, value, _titleSaveCts.Token);
}
private async System.Threading.Tasks.Task SaveTitleAsync(
TaskRowViewModel row, string value, CancellationToken ct)
{
try
{
await System.Threading.Tasks.Task.Delay(400, ct);
var title = value?.Trim();
// An empty title would make the row unidentifiable — keep the stored one.
if (string.IsNullOrEmpty(title)) return;
await using var ctx = _dbFactory.CreateDbContext();
var repo = new TaskRepository(ctx);
var entity = await repo.GetByIdAsync(row.Id);
if (entity is null || entity.Title == title) return;
entity.Title = title;
await repo.UpdateAsync(entity);
ct.ThrowIfCancellationRequested();
row.Title = title;
if (ReferenceEquals(Task, row))
Merge.SyncTaskContext(row.Id, title, row.IsPlanningParent);
}
catch (OperationCanceledException) { }
catch { }
}
partial void OnEditableDescriptionChanged(string value) partial void OnEditableDescriptionChanged(string value)
{ {
if (_suppressDescSave || Task is null) return; if (_suppressDescSave || Task is null) return;
@@ -525,7 +561,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (entity == null) return; if (entity == null) return;
EditableTitle = entity.Title; _suppressTitleSave = true;
try { EditableTitle = entity.Title; }
finally { _suppressTitleSave = false; }
_suppressDescSave = true; _suppressDescSave = true;
try { EditableDescription = entity.Description ?? ""; } try { EditableDescription = entity.Description ?? ""; }
finally { _suppressDescSave = false; } finally { _suppressDescSave = false; }