import: step to the next unsaved row below, not the first
A Save went back to the first unsaved row, so a skipped one came straight back;
now it takes the next unsaved row below, and skipping stays on track.
The walk comes round to what was passed once nothing's left below to save,
and a row renamed on the way in is followed from the place its new name gave.

Assisted-by: Claude Opus 5.5 via Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
change sqnmpqnvkpovnwspkvtppryptwzprmzk
commit d1f24aeaa2ed97294d13b3c800d79bcb449169cc
author Alpha Chen <alpha@kejadlen.dev>
date
parent xopslkqn
diff --git a/lib/pro_tacts/web/import.rb b/lib/pro_tacts/web/import.rb
index 11bb13c..5174658 100644
--- a/lib/pro_tacts/web/import.rb
+++ b/lib/pro_tacts/web/import.rb
@@ -545,16 +545,21 @@ module ProTacts
       # thing.
       return close_walk(r, upload) if saved.length + 1 == revised.length
 
-      # Onto the next row nobody has read, in the list's order
-      # (#walk_order) —
-      # the walk steps from card to card, a screen between saves
-      # having only ever repeated what the list beside every card
-      # says. `saved` was read before this write, so the row just
-      # written is named here too; a `fetch` rather than a `first`
-      # because empty is the case the line above took, and reaching
-      # it is a broken assumption rather than a screen to render.
-      remaining = walk_order(revised).reject { |i| saved.key?(i.to_s) || i == index }
-      r.redirect "/import/#{upload}/#{remaining.fetch(0)}", 303
+      # Onto the next row still to save below this one, in the list's
+      # order (#walk_order) — the walk steps from card to card, a
+      # screen between saves having only ever repeated what the list
+      # beside every card says. Below rather than first, so a row
+      # passed over stays passed over: skipping one is opening the
+      # next, and the walk comes back round to it only once the rows
+      # after it are saved. `saved` was read before this write, so the
+      # row just written is named here too; a `fetch` rather than a
+      # `first` because empty is the case the line above took, and
+      # reaching it is a broken assumption rather than a screen to
+      # render.
+      order = walk_order(revised)
+      remaining = order.reject { |i| saved.key?(i.to_s) || i == index }
+      below = order.drop(order.index(index).to_i + 1)
+      r.redirect "/import/#{upload}/#{below.find { remaining.include?(it) } || remaining.fetch(0)}", 303
     end
 
     # The end of the walk: the file and the walk's own notes gone, and
diff --git a/test/pro_tacts/admin/test_import_pages.rb b/test/pro_tacts/admin/test_import_pages.rb
index 621bca6..549a621 100644
--- a/test/pro_tacts/admin/test_import_pages.rb
+++ b/test/pro_tacts/admin/test_import_pages.rb
@@ -209,6 +209,24 @@ class AdminImportPagesTest < Minitest::Test
     end
   end
 
+  # A Save steps to the next unsaved row below it, so a row skipped
+  # by opening a later one stays skipped; with nothing unsaved below,
+  # the walk comes back round to the first one left.
+  def test_a_save_steps_past_a_skipped_row
+    with_contacts({}) do |_store|
+      zed = PLAIN.sub("N:Booles;Sam;;;", "N:Zed;Zoe;;;").sub("FN:Sam Booles", "FN:Zoe Zed")
+      adams = PLAIN.sub("N:Booles;Sam;;;", "N:adams;Al;;;").sub("FN:Sam Booles", "FN:Al adams")
+      id = upload(zed + JANE + adams)
+
+      # The list reads Al (2), Jane (1), Zoe (0); Al is skipped.
+      save(id, 1, first: "Jane", last: "Booles")
+      assert_equal "/import/#{id}/0", last_response.headers["location"]
+
+      save(id, 0, first: "Zoe", last: "Zed")
+      assert_equal "/import/#{id}/2", last_response.headers["location"]
+    end
+  end
+
   # Everything is unsaved until its own screen says otherwise, and a
   # walk broken off overnight has to say which rows those are. The
   # rail says it on screen; the word is there for a reader that