Consolidate note-drawing into Measure; add edit/delete
CI / check (push) Successful in 39s
CI / deploy (push) Successful in 13s

Measure already draws distance lines and area polygons by hand — a
second click-to-draw UI in the notes tool (Point/Area mode, from the
previous commit) duplicated that. Replaced it with a "Save as note"
button on the Measure panel that saves whatever shape is currently
drawn there; NotesControl goes back to being just a one-click point
pin. Both share a single NoteSaveForm (notes.js) for the note-text +
target-layer-file UI instead of two copies of it.

gpx.js: the "lines" source now also renders a note-flagged LineString
(dashed, popup-on-click) alongside the Polygon fill/outline from
before — the plain track-line layer excludes category:"Note" features
so a saved distance note doesn't also get the track hover-summary/
profile-panel treatment.

Notes are now individually editable/deletable from their own popup
(logged-in only): Edit swaps the popup content for an inline
textarea, Delete confirms and removes. Both target the feature by a
new client-generated properties.id rather than array position (new
GpxStore#update_feature/#delete_feature, PUT/DELETE /api/gpx/notes),
since neither array index nor the note's own text (the thing being
edited) is a stable identifier.

Verified live: Measure's Save button (disabled/enabled state, saves
the exact on-screen shape), the popup's Edit/Delete buttons and inline
edit form. Full test suite: 115 runs, 0 failures (new coverage for
update_feature/delete_feature and the PUT/DELETE routes).
This commit is contained in:
randy committed 2026-09-09 11:39:36 +02:00
1 parent db58468187
commit aafb4cb4a0
9 files changed
+499 -239

No files matched your search

+36 -2
View File
@@ -205,10 +205,44 @@ class GpxStoreTest < Minitest::Test
assert_equal "notes.geojson", path
end
def test_update_feature_replaces_only_the_matching_id
path = @store.append_feature(feature: sample_feature("Turning spot", id: "a"), folder: "", name: "My Notes")
@store.append_feature(feature: sample_feature("Narrow gate", id: "b"), path: path)
@store.update_feature(path: path, id: "a", feature: sample_feature("Turning spot (edited)", id: "a"))
names = JSON.parse(@store.geojson(path))["features"].map { |f| f["properties"]["name"] }
assert_equal ["Turning spot (edited)", "Narrow gate"], names
end
def test_update_feature_missing_file_raises_enoent
assert_raises(Errno::ENOENT) { @store.update_feature(path: "nope.geojson", id: "a", feature: sample_feature("x", id: "a")) }
end
def test_update_feature_missing_id_raises_enoent
path = @store.append_feature(feature: sample_feature("Turning spot", id: "a"), folder: "", name: "My Notes")
assert_raises(Errno::ENOENT) { @store.update_feature(path: path, id: "nope", feature: sample_feature("x", id: "a")) }
end
def test_delete_feature_removes_only_the_matching_id
path = @store.append_feature(feature: sample_feature("Turning spot", id: "a"), folder: "", name: "My Notes")
@store.append_feature(feature: sample_feature("Narrow gate", id: "b"), path: path)
@store.delete_feature(path: path, id: "a")
names = JSON.parse(@store.geojson(path))["features"].map { |f| f["properties"]["name"] }
assert_equal ["Narrow gate"], names
end
def test_delete_feature_missing_id_raises_enoent
path = @store.append_feature(feature: sample_feature("Turning spot", id: "a"), folder: "", name: "My Notes")
assert_raises(Errno::ENOENT) { @store.delete_feature(path: path, id: "nope") }
end
private
def sample_feature(name)
{ "type" => "Feature", "properties" => { "name" => name, "category" => "Note" },
def sample_feature(name, id: nil)
{ "type" => "Feature", "properties" => { "name" => name, "category" => "Note", "id" => id },
"geometry" => { "type" => "Point", "coordinates" => [17.1, 48.1] } }
end