Fix reorder: normalize SortOrder after move, remove AsNoTracking from write path

The shift-in-place approach broke when SortOrder values had gaps from soft
deletes. Switch to remove/insert then assign SortOrder = index, which is
correct regardless of initial values. Also fix OutgosController Reorder
which had AsNoTracking applied from the M-3 change, silently dropping saves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Spencer Twaddle
2026-05-06 22:24:30 -05:00
parent b82b3939ce
commit 4fa35dadc3
2 changed files with 9 additions and 11 deletions
@@ -105,11 +105,10 @@ public class IncomesController(AppDbContext db, BudgetAuthorizationService authz
var oldIdx = incomes.IndexOf(item); var oldIdx = incomes.IndexOf(item);
var newIdx = Math.Clamp(req.NewIndex, 0, incomes.Count - 1); var newIdx = Math.Clamp(req.NewIndex, 0, incomes.Count - 1);
if (oldIdx == newIdx) return NoContent(); if (oldIdx == newIdx) return NoContent();
if (newIdx < oldIdx) incomes.RemoveAt(oldIdx);
for (int i = newIdx; i < oldIdx; i++) incomes[i].SortOrder++; incomes.Insert(newIdx, item);
else for (int i = 0; i < incomes.Count; i++)
for (int i = oldIdx + 1; i <= newIdx; i++) incomes[i].SortOrder--; incomes[i].SortOrder = i;
item.SortOrder = newIdx;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return NoContent(); return NoContent();
} }
@@ -116,17 +116,16 @@ public class OutgosController(AppDbContext db, BudgetAuthorizationService authz)
{ {
if (TryGetUserId(out var userId) is { } err) return err; if (TryGetUserId(out var userId) is { } err) return err;
if (!await authz.CanWriteAsync(budgetId, userId)) return Forbid(); if (!await authz.CanWriteAsync(budgetId, userId)) return Forbid();
var outgos = await db.Outgos.AsNoTracking().Where(o => o.BudgetId == budgetId).OrderBy(o => o.SortOrder).ToListAsync(); var outgos = await db.Outgos.Where(o => o.BudgetId == budgetId).OrderBy(o => o.SortOrder).ToListAsync();
var item = outgos.FirstOrDefault(o => o.Id == req.Id); var item = outgos.FirstOrDefault(o => o.Id == req.Id);
if (item is null) return NotFound(); if (item is null) return NotFound();
var oldIdx = outgos.IndexOf(item); var oldIdx = outgos.IndexOf(item);
var newIdx = Math.Clamp(req.NewIndex, 0, outgos.Count - 1); var newIdx = Math.Clamp(req.NewIndex, 0, outgos.Count - 1);
if (oldIdx == newIdx) return NoContent(); if (oldIdx == newIdx) return NoContent();
if (newIdx < oldIdx) outgos.RemoveAt(oldIdx);
for (int i = newIdx; i < oldIdx; i++) outgos[i].SortOrder++; outgos.Insert(newIdx, item);
else for (int i = 0; i < outgos.Count; i++)
for (int i = oldIdx + 1; i <= newIdx; i++) outgos[i].SortOrder--; outgos[i].SortOrder = i;
item.SortOrder = newIdx;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return NoContent(); return NoContent();
} }