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>
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