]> git.openstreetmap.org Git - rails.git/commitdiff
Merge pull request #5591 from AntonKhorev/api-element-resources--index-paths
authorAndy Allan <git@gravitystorm.co.uk>
Wed, 5 Feb 2025 17:13:47 +0000 (17:13 +0000)
committerGitHub <noreply@github.com>
Wed, 5 Feb 2025 17:13:47 +0000 (17:13 +0000)
API element resources - index paths

36 files changed:
Gemfile.lock
app/assets/javascripts/application.js
app/assets/javascripts/auth_providers.js
app/assets/javascripts/edit/id.js.erb
app/assets/javascripts/embed.js.erb
app/assets/javascripts/index.js
app/assets/javascripts/index/contextmenu.js
app/assets/javascripts/index/directions-endpoint.js
app/assets/javascripts/index/directions.js
app/assets/javascripts/index/export.js
app/assets/javascripts/index/new_note.js
app/assets/javascripts/index/query.js
app/assets/javascripts/index/search.js
app/assets/javascripts/leaflet.map.js
app/assets/javascripts/osm.js.erb
app/assets/javascripts/user.js
app/assets/stylesheets/common.scss
app/controllers/api/maps_controller.rb
app/helpers/note_helper.rb
app/helpers/social_share_button_helper.rb
app/models/note.rb
app/views/api/notes/_note.rss.builder
app/views/issues/_comments.html.erb
app/views/issues/_page.html.erb
app/views/notes/index.html.erb
app/views/notes/show.html.erb
config/eslint.js
config/locales/en.yml
lib/tasks/eslint.rake
package.json
test/controllers/api/changesets_controller_test.rb
test/helpers/social_share_button_helper_test.rb
test/javascripts/osm_test.js
test/system/create_note_test.rb
test/system/issues_test.rb
yarn.lock

index 761beefa3a0555cab7acda87d1585bb7b98df05e..0fd4dc5ea1662aa9055b3f31df547b9020149eb4 100644 (file)
@@ -60,7 +60,7 @@ GEM
       activemodel (= 7.2.2.1)
       activesupport (= 7.2.2.1)
       timeout (>= 0.4.0)
-    activerecord-import (2.0.0)
+    activerecord-import (2.1.0)
       activerecord (>= 4.2)
     activestorage (7.2.2.1)
       actionpack (= 7.2.2.1)
@@ -92,8 +92,8 @@ GEM
     autoprefixer-rails (10.4.19.0)
       execjs (~> 2)
     aws-eventstream (1.3.0)
-    aws-partitions (1.1043.0)
-    aws-sdk-core (3.217.0)
+    aws-partitions (1.1044.0)
+    aws-sdk-core (3.217.1)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -101,7 +101,7 @@ GEM
     aws-sdk-kms (1.97.0)
       aws-sdk-core (~> 3, >= 3.216.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.178.0)
+    aws-sdk-s3 (1.179.0)
       aws-sdk-core (~> 3, >= 3.216.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
@@ -237,7 +237,7 @@ GEM
       dry-logic (>= 1.4, < 2)
       dry-types (>= 1.7, < 2)
       zeitwerk (~> 2.6)
-    dry-types (1.8.1)
+    dry-types (1.8.2)
       bigdecimal (~> 3.0)
       concurrent-ruby (~> 1.0)
       dry-core (~> 1.0)
@@ -260,8 +260,8 @@ GEM
     erubi (1.13.1)
     execjs (2.10.0)
     exifr (1.4.1)
-    factory_bot (6.5.0)
-      activesupport (>= 5.0.0)
+    factory_bot (6.5.1)
+      activesupport (>= 6.1.0)
     factory_bot_rails (6.4.4)
       factory_bot (~> 6.5)
       railties (>= 5.0.0)
@@ -451,7 +451,7 @@ GEM
     open4 (1.3.4)
     openstreetmap-deadlock_retry (1.3.1)
     ostruct (0.6.1)
-    overcommit (0.65.0)
+    overcommit (0.66.0)
       childprocess (>= 0.6.3, < 6)
       iniparse (~> 1.4)
       rexml (>= 3.3.9)
@@ -549,14 +549,14 @@ GEM
     rouge (4.5.1)
     rtlcss (0.2.1)
       mini_racer (>= 0.6.3)
-    rubocop (1.71.0)
+    rubocop (1.71.1)
       json (~> 2.3)
       language_server-protocol (>= 3.17.0)
       parallel (~> 1.10)
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 2.9.3, < 3.0)
-      rubocop-ast (>= 1.36.2, < 2.0)
+      rubocop-ast (>= 1.38.0, < 2.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 2.4.0, < 4.0)
     rubocop-ast (1.38.0)
index 29563cb4a0020fa520d56ddcd1c0f0f05a78b0d8..e8662e5cff4d45ffe47f9432cf4b13b939b27fb0 100644 (file)
@@ -14,7 +14,6 @@
 //= require oauth
 //= require matomo
 //= require richtext
-//= require qs/dist/qs
 
 {
   const application_data = $("head").data();
  */
 window.updateLinks = function (loc, zoom, layers, object) {
   $(".geolink").each(function (index, link) {
-    var href = link.href.split(/[?#]/)[0],
-        args = Qs.parse(link.search.substring(1)),
-        editlink = $(link).hasClass("editlink");
+    let href = link.href.split(/[?#]/)[0];
+    const queryArgs = new URLSearchParams(link.search),
+          editlink = $(link).hasClass("editlink");
 
-    delete args.node;
-    delete args.way;
-    delete args.relation;
-    delete args.changeset;
-    delete args.note;
+    queryArgs.delete("node");
+    queryArgs.delete("way");
+    queryArgs.delete("relation");
+    queryArgs.delete("changeset");
+    queryArgs.delete("note");
 
     if (object && editlink) {
-      args[object.type] = object.id;
+      queryArgs.set(object.type, object.id);
     }
 
-    var query = Qs.stringify(args);
+    const query = queryArgs.toString();
     if (query) href += "?" + query;
 
-    args = {
+    const hashArgs = {
       lat: loc.lat,
       lon: "lon" in loc ? loc.lon : loc.lng,
       zoom: zoom
     };
 
     if (layers && !editlink) {
-      args.layers = layers;
+      hashArgs.layers = layers;
     }
 
-    href += OSM.formatHash(args);
+    href += OSM.formatHash(hashArgs);
 
     link.href = href;
   });
index d6a7c6393c677254bccc7e18e763219ce841bf57..18c3e38e0776d85228ce73af56dbc5ee6a16fe61 100644 (file)
@@ -1,10 +1,8 @@
-//= require qs/dist/qs
-
 $(document).ready(function () {
   // Attach referer to authentication buttons
   $(".auth_button").each(function () {
-    var params = Qs.parse(this.search.substring(1));
-    params.referer = $("#referer").val();
-    this.search = Qs.stringify(params);
+    const params = new URLSearchParams(this.search);
+    params.set("referer", $("#referer").val());
+    this.search = params.toString();
   });
 });
index 08e04dcf377dacb0f868fde3a52fae1311cb4feb..3c8b657cab6771824a34af5ca9961b2fd95e8896 100644 (file)
@@ -1,5 +1,3 @@
-//= require qs/dist/qs
-
 $(document).ready(function () {
   var id = $("#id-embed");
 
@@ -7,45 +5,45 @@ $(document).ready(function () {
     var hash = location.hash.substring(1);
     var hashParams = hash ? OSM.params(hash) : {};
     var mapParams = OSM.mapParams();
-    var params = {};
+    var params = new URLSearchParams();
 
     if (mapParams.object) {
-      params.id = mapParams.object.type + "/" + mapParams.object.id;
+      params.set("id", mapParams.object.type + "/" + mapParams.object.id);
       mapParams = OSM.parseHash(location.hash);
       if (mapParams.center) {
-        params.map = mapParams.zoom + "/" + mapParams.center.lat + "/" + mapParams.center.lng;
+        params.set("map", mapParams.zoom + "/" + mapParams.center.lat + "/" + mapParams.center.lng);
       }
     } else if (id.data("lat") && id.data("lon")) {
-      params.map = "16/" + id.data("lat") + "/" + id.data("lon");
+      params.set("map", "16/" + id.data("lat") + "/" + id.data("lon"));
     } else {
-      params.map = (mapParams.zoom || 17) + "/" + mapParams.lat + "/" + mapParams.lon;
+      params.set("map", (mapParams.zoom || 17) + "/" + mapParams.lat + "/" + mapParams.lon);
     }
 
-    if (hashParams.background) params.background = hashParams.background;
-    if (hashParams.comment) params.comment = hashParams.comment;
-    if (hashParams.disable_features) params.disable_features = hashParams.disable_features;
-    if (hashParams.hashtags) params.hashtags = hashParams.hashtags;
-    if (hashParams.locale) params.locale = hashParams.locale;
-    if (hashParams.maprules) params.maprules = hashParams.maprules;
-    if (hashParams.offset) params.offset = hashParams.offset;
-    if (hashParams.photo) params.photo = hashParams.photo;
-    if (hashParams.photo_dates) params.photo_dates = hashParams.photo_dates;
-    if (hashParams.photo_overlay) params.photo_overlay = hashParams.photo_overlay;
-    if (hashParams.photo_username) params.photo_username = hashParams.photo_username;
-    if (hashParams.presets) params.presets = hashParams.presets;
-    if (hashParams.source) params.source = hashParams.source;
-    if (hashParams.validationDisable) params.validationDisable = hashParams.validationDisable;
-    if (hashParams.validationWarning) params.validationWarning = hashParams.validationWarning;
-    if (hashParams.validationError) params.validationError = hashParams.validationError;
-    if (hashParams.walkthrough) params.walkthrough = hashParams.walkthrough;
+    if (hashParams.background) params.set("background", hashParams.background);
+    if (hashParams.comment) params.set("comment", hashParams.comment);
+    if (hashParams.disable_features) params.set("disable_features", hashParams.disable_features);
+    if (hashParams.hashtags) params.set("hashtags", hashParams.hashtags);
+    if (hashParams.locale) params.set("locale", hashParams.locale);
+    if (hashParams.maprules) params.set("maprules", hashParams.maprules);
+    if (hashParams.offset) params.set("offset", hashParams.offset);
+    if (hashParams.photo) params.set("photo", hashParams.photo);
+    if (hashParams.photo_dates) params.set("photo_dates", hashParams.photo_dates);
+    if (hashParams.photo_overlay) params.set("photo_overlay", hashParams.photo_overlay);
+    if (hashParams.photo_username) params.set("photo_username", hashParams.photo_username);
+    if (hashParams.presets) params.set("presets", hashParams.presets);
+    if (hashParams.source) params.set("source", hashParams.source);
+    if (hashParams.validationDisable) params.set("validationDisable", hashParams.validationDisable);
+    if (hashParams.validationWarning) params.set("validationWarning", hashParams.validationWarning);
+    if (hashParams.validationError) params.set("validationError", hashParams.validationError);
+    if (hashParams.walkthrough) params.set("walkthrough", hashParams.walkthrough);
 
     if (id.data("gpx")) {
-      params.gpx = id.data("gpx");
+      params.set("gpx", id.data("gpx"));
     } else if (hashParams.gpx) {
-      params.gpx = hashParams.gpx;
+      params.set("gpx", hashParams.gpx);
     }
 
-    id.attr("src", id.data("url") + "#" + Qs.stringify(params));
+    id.attr("src", id.data("url") + "#" + params);
   } else {
     alert(I18n.t("site.edit.id_not_configured"));
   }
index 44bb1fef5dbd7dabfc465a2f46ca34ff3a34ac81..34a8b68bcf8740e880fcb6e7574ce8adc169e75b 100644 (file)
@@ -36,12 +36,14 @@ window.onload = function () {
   map.attributionControl.setPrefix("");
   map.removeControl(map.attributionControl);
 
+  const isDarkTheme = args.theme === "dark" || (args.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
   const layers = <%=
     YAML.load_file(Rails.root.join("config/layers.yml"))
       .select { |entry| entry["canEmbed"] }
       .each_with_object({}) do |entry, obj|
         obj[entry["layerId"]] = {
           layer: entry["leafletOsmId"],
+          darkLayer: entry["leafletOsmDarkId"],
           apiKeyId: entry["apiKeyId"]
         }.compact
       end.to_json
@@ -49,7 +51,7 @@ window.onload = function () {
   const layerId = (args.layer || "").replaceAll(" ", "");
   const layerConfig = layers[layerId] || layers.mapnik;
   const { layer, ...options } = {
-    layer: layerConfig.layer,
+    layer: layerConfig.darkLayer && isDarkTheme ? layerConfig.darkLayer : layerConfig.layer,
     apikey: apiKeys[layerConfig.apiKeyId],
     ...tileOptions[layerId]
   };
index d61191fb43ab3e669417c7dd2c2246a65b969501..ad34479d32f93b46a739e898f081e7c3cffc65ed 100644 (file)
@@ -22,7 +22,6 @@
 //= require index/changeset
 //= require index/query
 //= require router
-//= require qs/dist/qs
 
 $(document).ready(function () {
   var map = new L.OSM.Map("map", {
@@ -239,18 +238,18 @@ $(document).ready(function () {
   function remoteEditHandler(bbox, object) {
     var remoteEditHost = "http://127.0.0.1:8111",
         osmHost = location.protocol + "//" + location.host,
-        query = {
+        query = new URLSearchParams({
           left: bbox.getWest() - 0.0001,
           top: bbox.getNorth() + 0.0001,
           right: bbox.getEast() + 0.0001,
           bottom: bbox.getSouth() - 0.0001
-        };
+        });
 
-    if (object && object.type !== "note") query.select = object.type + object.id; // can't select notes
-    sendRemoteEditCommand(remoteEditHost + "/load_and_zoom?" + Qs.stringify(query), function () {
+    if (object && object.type !== "note") query.set("select", object.type + object.id); // can't select notes
+    sendRemoteEditCommand(remoteEditHost + "/load_and_zoom?" + query, function () {
       if (object && object.type === "note") {
-        var noteQuery = { url: osmHost + OSM.apiUrl(object) };
-        sendRemoteEditCommand(remoteEditHost + "/import?" + Qs.stringify(noteQuery));
+        const noteQuery = new URLSearchParams({ url: osmHost + OSM.apiUrl(object) });
+        sendRemoteEditCommand(remoteEditHost + "/import?" + noteQuery);
       }
     });
 
@@ -294,9 +293,9 @@ $(document).ready(function () {
     };
 
     page.load = function () {
-      var params = Qs.parse(location.search.substring(1));
-      if (params.query) {
-        $("#sidebar .search_form input[name=query]").value(params.query);
+      const params = new URLSearchParams(location.search);
+      if (params.has("query")) {
+        $("#sidebar .search_form input[name=query]").value(params.get("query"));
       }
       if (!("autofocus" in document.createElement("input"))) {
         $("#sidebar .search_form input[name=query]").focus();
index ea284f29b97d87f0ee2508697b6afb1228c8a5b2..fea7e73145415ecf36bca270b53f19feab5683f3 100644 (file)
@@ -1,16 +1,11 @@
-//= require qs/dist/qs
-
 OSM.initializeContextMenu = function (map) {
   map.contextmenu.addItem({
     text: I18n.t("javascripts.context.directions_from"),
     callback: function directionsFromHere(e) {
-      var precision = OSM.zoomPrecision(map.getZoom()),
-          latlng = e.latlng.wrap(),
-          lat = latlng.lat.toFixed(precision),
-          lng = latlng.lng.toFixed(precision);
+      const latlng = OSM.cropLocation(e.latlng, map.getZoom());
 
-      OSM.router.route("/directions?" + Qs.stringify({
-        from: lat + "," + lng,
+      OSM.router.route("/directions?" + new URLSearchParams({
+        from: latlng.join(","),
         to: getDirectionsEndpointCoordinatesFromInput($("#route_to"))
       }));
     }
@@ -19,14 +14,11 @@ OSM.initializeContextMenu = function (map) {
   map.contextmenu.addItem({
     text: I18n.t("javascripts.context.directions_to"),
     callback: function directionsToHere(e) {
-      var precision = OSM.zoomPrecision(map.getZoom()),
-          latlng = e.latlng.wrap(),
-          lat = latlng.lat.toFixed(precision),
-          lng = latlng.lng.toFixed(precision);
+      const latlng = OSM.cropLocation(e.latlng, map.getZoom());
 
-      OSM.router.route("/directions?" + Qs.stringify({
+      OSM.router.route("/directions?" + new URLSearchParams({
         from: getDirectionsEndpointCoordinatesFromInput($("#route_from")),
-        to: lat + "," + lng
+        to: latlng.join(",")
       }));
     }
   });
@@ -34,36 +26,27 @@ OSM.initializeContextMenu = function (map) {
   map.contextmenu.addItem({
     text: I18n.t("javascripts.context.add_note"),
     callback: function addNoteHere(e) {
-      var precision = OSM.zoomPrecision(map.getZoom()),
-          latlng = e.latlng.wrap(),
-          lat = latlng.lat.toFixed(precision),
-          lng = latlng.lng.toFixed(precision);
+      const [lat, lon] = OSM.cropLocation(e.latlng, map.getZoom());
 
-      OSM.router.route("/note/new?lat=" + lat + "&lon=" + lng);
+      OSM.router.route("/note/new?" + new URLSearchParams({ lat, lon }));
     }
   });
 
   map.contextmenu.addItem({
     text: I18n.t("javascripts.context.show_address"),
     callback: function describeLocation(e) {
-      var precision = OSM.zoomPrecision(map.getZoom()),
-          latlng = e.latlng.wrap(),
-          lat = latlng.lat.toFixed(precision),
-          lng = latlng.lng.toFixed(precision);
+      const [lat, lon] = OSM.cropLocation(e.latlng, map.getZoom());
 
-      OSM.router.route("/search?lat=" + encodeURIComponent(lat) + "&lon=" + encodeURIComponent(lng));
+      OSM.router.route("/search?" + new URLSearchParams({ lat, lon }));
     }
   });
 
   map.contextmenu.addItem({
     text: I18n.t("javascripts.context.query_features"),
     callback: function queryFeatures(e) {
-      var precision = OSM.zoomPrecision(map.getZoom()),
-          latlng = e.latlng.wrap(),
-          lat = latlng.lat.toFixed(precision),
-          lng = latlng.lng.toFixed(precision);
+      const [lat, lon] = OSM.cropLocation(e.latlng, map.getZoom());
 
-      OSM.router.route("/query?lat=" + lat + "&lon=" + lng);
+      OSM.router.route("/query?" + new URLSearchParams({ lat, lon }));
     }
   });
 
index 7fe4b4ea7f4803053f816d2f9d7986817e798aab..30d499ad731707b404dc1ca3448af2e215cfb7d7 100644 (file)
@@ -34,11 +34,15 @@ OSM.DirectionsEndpoint = function Endpoint(map, input, iconUrl, dragCallback, ch
   };
 
   function markerDragListener(e) {
-    var latlng = convertLatLngToZoomPrecision(e.target.getLatLng());
+    const latlng = L.latLng(OSM.cropLocation(e.target.getLatLng(), map.getZoom()));
+
+    if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
+    delete endpoint.geocodeRequest;
 
     setLatLng(latlng);
     setInputValueFromLatLng(latlng);
     endpoint.value = input.val();
+    if (e.type === "dragend") getReverseGeocode();
     dragCallback(e.type === "drag");
   }
 
@@ -52,26 +56,54 @@ OSM.DirectionsEndpoint = function Endpoint(map, input, iconUrl, dragCallback, ch
     endpoint.setValue(value);
   }
 
-  endpoint.setValue = function (value, latlng) {
+  endpoint.setValue = function (value) {
+    if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
+    delete endpoint.geocodeRequest;
+    input.removeClass("is-invalid");
+
+    var coordinatesMatch = value.match(/^\s*([+-]?\d+(?:\.\d*)?)(?:\s+|\s*[/,]\s*)([+-]?\d+(?:\.\d*)?)\s*$/);
+    var latlng = coordinatesMatch && L.latLng(coordinatesMatch[1], coordinatesMatch[2]);
+
+    if (latlng && endpoint.cachedReverseGeocode && endpoint.cachedReverseGeocode.latlng.equals(latlng)) {
+      setLatLng(latlng);
+      if (endpoint.cachedReverseGeocode.notFound) {
+        endpoint.value = value;
+        input.addClass("is-invalid");
+      } else {
+        endpoint.value = endpoint.cachedReverseGeocode.value;
+      }
+      input.val(endpoint.value);
+      changeCallback();
+      return;
+    }
+
     endpoint.value = value;
     removeLatLng();
-    input.removeClass("is-invalid");
     input.val(value);
 
     if (latlng) {
       setLatLng(latlng);
       setInputValueFromLatLng(latlng);
+      getReverseGeocode();
       changeCallback();
     } else if (endpoint.value) {
       getGeocode();
     }
   };
 
+  endpoint.swapCachedReverseGeocodes = function (otherEndpoint) {
+    var g0 = endpoint.cachedReverseGeocode;
+    var g1 = otherEndpoint.cachedReverseGeocode;
+    delete endpoint.cachedReverseGeocode;
+    delete otherEndpoint.cachedReverseGeocode;
+    if (g0) otherEndpoint.cachedReverseGeocode = g0;
+    if (g1) endpoint.cachedReverseGeocode = g1;
+  };
+
   function getGeocode() {
     var viewbox = map.getBounds().toBBoxString(); // <sw lon>,<sw lat>,<ne lon>,<ne lat>
     var geocodeUrl = OSM.NOMINATIM_URL + "search?q=" + encodeURIComponent(endpoint.value) + "&format=json&viewbox=" + viewbox;
 
-    if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
     endpoint.geocodeRequest = $.getJSON(geocodeUrl, function (json) {
       delete endpoint.geocodeRequest;
       if (json.length === 0) {
@@ -82,12 +114,30 @@ OSM.DirectionsEndpoint = function Endpoint(map, input, iconUrl, dragCallback, ch
 
       setLatLng(L.latLng(json[0]));
 
+      endpoint.value = json[0].display_name;
       input.val(json[0].display_name);
 
       changeCallback();
     });
   }
 
+  function getReverseGeocode() {
+    var latlng = endpoint.latlng.clone();
+    var reverseGeocodeUrl = OSM.NOMINATIM_URL + "reverse?lat=" + latlng.lat + "&lon=" + latlng.lng + "&format=json";
+
+    endpoint.geocodeRequest = $.getJSON(reverseGeocodeUrl, function (json) {
+      delete endpoint.geocodeRequest;
+      if (!json || !json.display_name) {
+        endpoint.cachedReverseGeocode = { latlng: latlng, notFound: true };
+        return;
+      }
+
+      endpoint.value = json.display_name;
+      input.val(json.display_name);
+      endpoint.cachedReverseGeocode = { latlng: latlng, value: endpoint.value };
+    });
+  }
+
   function setLatLng(ll) {
     input
       .attr("data-lat", ll.lat)
@@ -109,11 +159,5 @@ OSM.DirectionsEndpoint = function Endpoint(map, input, iconUrl, dragCallback, ch
     input.val(latlng.lat + ", " + latlng.lng);
   }
 
-  function convertLatLngToZoomPrecision(latlng) {
-    var precision = OSM.zoomPrecision(map.getZoom());
-
-    return L.latLng(latlng.lat.toFixed(precision), latlng.lng.toFixed(precision));
-  }
-
   return endpoint;
 };
index 391c1f9315c7fe945d822d49380f9578729e79c0..acba02d837e834ee77469141477e2e3048092958 100644 (file)
@@ -1,7 +1,6 @@
 //= require ./directions-endpoint
 //= require_self
 //= require_tree ./directions
-//= require qs/dist/qs
 
 OSM.Directions = function (map) {
   var routeRequest = null; // jqXHR object of an ongoing route request or null
@@ -65,10 +64,9 @@ OSM.Directions = function (map) {
     if (coordTo) {
       routeTo = coordTo.lat + "," + coordTo.lng;
     }
+    endpoints[0].swapCachedReverseGeocodes(endpoints[1]);
 
-    OSM.router.route("/directions?" + Qs.stringify({
-      from: $("#route_to").val(),
-      to: $("#route_from").val(),
+    OSM.router.route("/directions?" + new URLSearchParams({
       route: routeTo + ";" + routeFrom
     }));
   });
@@ -116,18 +114,14 @@ OSM.Directions = function (map) {
     // Cancel any route that is already in progress
     if (routeRequest) routeRequest.abort();
 
-    var o = endpoints[0].latlng,
-        d = endpoints[1].latlng;
+    const points = endpoints.map(p => p.latlng);
 
-    if (!o || !d) return;
+    if (!points[0] || !points[1]) return;
     $("header").addClass("closed");
 
-    var precision = OSM.zoomPrecision(map.getZoom());
-
-    OSM.router.replace("/directions?" + Qs.stringify({
+    OSM.router.replace("/directions?" + new URLSearchParams({
       engine: chosenEngine.id,
-      route: o.lat.toFixed(precision) + "," + o.lng.toFixed(precision) + ";" +
-             d.lat.toFixed(precision) + "," + d.lng.toFixed(precision)
+      route: points.map(p => OSM.cropLocation(p, map.getZoom()).join()).join(";")
     }));
 
     // copy loading item to sidebar and display it. we copy it, rather than
@@ -136,7 +130,7 @@ OSM.Directions = function (map) {
     $("#sidebar_content").html($(".directions_form .loader_copy").html());
     map.setSidebarOverlaid(false);
 
-    routeRequest = chosenEngine.getRoute([o, d], function (err, route) {
+    routeRequest = chosenEngine.getRoute(points, function (err, route) {
       routeRequest = null;
 
       if (err) {
@@ -285,32 +279,28 @@ OSM.Directions = function (map) {
       var pt = L.DomEvent.getMousePosition(oe, map.getContainer()); // co-ordinates of the mouse pointer at present
       pt.y += 20;
       var ll = map.containerPointToLatLng(pt);
-      var precision = OSM.zoomPrecision(map.getZoom());
-      var value = ll.lat.toFixed(precision) + ", " + ll.lng.toFixed(precision);
-      var llWithPrecision = L.latLng(ll.lat.toFixed(precision), ll.lng.toFixed(precision));
-      endpoints[type === "from" ? 0 : 1].setValue(value, llWithPrecision);
+      const llWithPrecision = OSM.cropLocation(ll, map.getZoom());
+      endpoints[type === "from" ? 0 : 1].setValue(llWithPrecision.join(", "));
     });
 
     endpoints[0].enable();
     endpoints[1].enable();
 
-    var params = Qs.parse(location.search.substring(1)),
-        route = (params.route || "").split(";"),
-        from = route[0] && L.latLng(route[0].split(",")),
-        to = route[1] && L.latLng(route[1].split(","));
+    const params = new URLSearchParams(location.search),
+          route = (params.get("route") || "").split(";");
 
-    if (params.engine) {
-      var engineIndex = findEngine(params.engine);
+    if (params.has("engine")) {
+      var engineIndex = findEngine(params.get("engine"));
 
       if (engineIndex >= 0) {
         setEngine(engineIndex);
       }
     }
 
-    endpoints[0].setValue(params.from || "", from);
-    endpoints[1].setValue(params.to || "", to);
+    endpoints[0].setValue(params.get("from") || route[0] || "");
+    endpoints[1].setValue(params.get("to") || route[1] || "");
 
-    map.setSidebarOverlaid(!from || !to);
+    map.setSidebarOverlaid(!endpoints[0].latlng || !endpoints[1].latlng);
   };
 
   page.load = function () {
index a953920f895d8fe62cefafdd3927f90f040ea62d..3b23b9cbf3abba23e53375f89cabf95eeea2d9b3 100644 (file)
@@ -36,16 +36,16 @@ OSM.Export = function (map) {
   }
 
   function setBounds(bounds) {
-    var precision = OSM.zoomPrecision(map.getZoom());
-    $("#minlon").val(bounds.getWest().toFixed(precision));
-    $("#minlat").val(bounds.getSouth().toFixed(precision));
-    $("#maxlon").val(bounds.getEast().toFixed(precision));
-    $("#maxlat").val(bounds.getNorth().toFixed(precision));
+    const truncated = [bounds.getSouthWest(), bounds.getNorthEast()]
+      .map(c => OSM.cropLocation(c, map.getZoom()));
+    $("#minlon").val(truncated[0][1]);
+    $("#minlat").val(truncated[0][0]);
+    $("#maxlon").val(truncated[1][1]);
+    $("#maxlat").val(truncated[1][0]);
 
     $("#export_overpass").attr("href",
                                "https://overpass-api.de/api/map?bbox=" +
-                               $("#minlon").val() + "," + $("#minlat").val() + "," +
-                               $("#maxlon").val() + "," + $("#maxlat").val());
+                               truncated.map(p => p.reverse()).join());
   }
 
   function validateControls() {
index 4f38f8b55754267eab293788c51793df986a3064..a1511a3bc14b6184bb5777bb718e2ae9e22f478c 100644 (file)
@@ -1,5 +1,3 @@
-//= require qs/dist/qs
-
 OSM.NewNote = function (map) {
   var noteLayer = map.noteLayer,
       content = $("#sidebar_content"),
@@ -133,11 +131,11 @@ OSM.NewNote = function (map) {
 
     map.addLayer(noteLayer);
 
-    var params = Qs.parse(path.substring(path.indexOf("?") + 1));
+    const params = new URLSearchParams(path.substring(path.indexOf("?")));
     var markerLatlng;
 
-    if (params.lat && params.lon) {
-      markerLatlng = L.latLng(params.lat, params.lon);
+    if (params.has("lat") && params.has("lon")) {
+      markerLatlng = L.latLng(params.get("lat"), params.get("lon"));
     } else {
       markerLatlng = map.getCenter();
     }
index 09e4de31e0c6e78faf57444534b55fa3c5653292..3874dbdbb3ca5931068c6fb96a1b29c0f9f6e077 100644 (file)
@@ -1,5 +1,3 @@
-//= require qs/dist/qs
-
 OSM.Query = function (map) {
   var url = OSM.OVERPASS_URL,
       credentials = OSM.OVERPASS_CREDENTIALS,
@@ -272,18 +270,18 @@ OSM.Query = function (map) {
   function queryOverpass(lat, lng) {
     var latlng = L.latLng(lat, lng).wrap(),
         bounds = map.getBounds().wrap(),
-        precision = OSM.zoomPrecision(map.getZoom()),
-        bbox = bounds.getSouth().toFixed(precision) + "," +
-               bounds.getWest().toFixed(precision) + "," +
-               bounds.getNorth().toFixed(precision) + "," +
-               bounds.getEast().toFixed(precision),
-        radius = 10 * Math.pow(1.5, 19 - map.getZoom()),
-        around = "around:" + radius + "," + lat + "," + lng,
-        nodes = "node(" + around + ")",
-        ways = "way(" + around + ")",
-        relations = "relation(" + around + ")",
-        nearby = "(" + nodes + ";" + ways + ";);out tags geom(" + bbox + ");" + relations + ";out geom(" + bbox + ");",
-        isin = "is_in(" + lat + "," + lng + ")->.a;way(pivot.a);out tags bb;out ids geom(" + bbox + ");relation(pivot.a);out tags bb;";
+        zoom = map.getZoom(),
+        bbox = [bounds.getSouthWest(), bounds.getNorthEast()]
+          .map(c => OSM.cropLocation(c, zoom))
+          .join(),
+        geombbox = "geom(" + bbox + ");",
+        radius = 10 * Math.pow(1.5, 19 - zoom),
+        around = "(around:" + radius + "," + lat + "," + lng + ")",
+        nodes = "node" + around,
+        ways = "way" + around,
+        relations = "relation" + around,
+        nearby = "(" + nodes + ";" + ways + ";);out tags " + geombbox + relations + ";out " + geombbox,
+        isin = "is_in(" + lat + "," + lng + ")->.a;way(pivot.a);out tags bb;out ids " + geombbox + "relation(pivot.a);out tags bb;";
 
     $("#sidebar_content .query-intro")
       .hide();
@@ -299,12 +297,9 @@ OSM.Query = function (map) {
   }
 
   function clickHandler(e) {
-    var precision = OSM.zoomPrecision(map.getZoom()),
-        latlng = e.latlng.wrap(),
-        lat = latlng.lat.toFixed(precision),
-        lng = latlng.lng.toFixed(precision);
+    const [lat, lon] = OSM.cropLocation(e.latlng, map.getZoom());
 
-    OSM.router.route("/query?lat=" + lat + "&lon=" + lng);
+    OSM.router.route("/query?" + new URLSearchParams({ lat, lon }));
   }
 
   function enableQueryMode() {
@@ -329,8 +324,8 @@ OSM.Query = function (map) {
   };
 
   page.load = function (path, noCentre) {
-    var params = Qs.parse(path.substring(path.indexOf("?") + 1)),
-        latlng = L.latLng(params.lat, params.lon);
+    const params = new URLSearchParams(path.substring(path.indexOf("?"))),
+          latlng = L.latLng(params.get("lat"), params.get("lon"));
 
     if (!window.location.hash && !noCentre && !map.getBounds().contains(latlng)) {
       OSM.router.withoutMoveListener(function () {
@@ -338,7 +333,7 @@ OSM.Query = function (map) {
       });
     }
 
-    queryOverpass(params.lat, params.lon);
+    queryOverpass(params.get("lat"), params.get("lon"));
   };
 
   page.unload = function (sameController) {
index 2bfbb2e1c6d4fa9bc18c7264cdbb65651785899a..e54c5132c65ab4dca495aa72bfc9ed380718df7b 100644 (file)
@@ -1,5 +1,3 @@
-//= require qs/dist/qs
-
 OSM.Search = function (map) {
   $(".search_form input[name=query]").on("input", function (e) {
     if ($(e.target).val() === "") {
@@ -33,12 +31,9 @@ OSM.Search = function (map) {
   $(".describe_location").on("click", function (e) {
     e.preventDefault();
     $("header").addClass("closed");
-    var center = map.getCenter().wrap(),
-        precision = OSM.zoomPrecision(map.getZoom()),
-        lat = center.lat.toFixed(precision),
-        lng = center.lng.toFixed(precision);
+    const [lat, lon] = OSM.cropLocation(map.getCenter(), map.getZoom());
 
-    OSM.router.route("/search?lat=" + encodeURIComponent(lat) + "&lon=" + encodeURIComponent(lng));
+    OSM.router.route("/search?" + new URLSearchParams({ lat, lon }));
   });
 
   $("#sidebar_content")
@@ -118,12 +113,12 @@ OSM.Search = function (map) {
   var page = {};
 
   page.pushstate = page.popstate = function (path) {
-    var params = Qs.parse(path.substring(path.indexOf("?") + 1));
-    if (params.query) {
-      $(".search_form input[name=query]").val(params.query);
+    const params = new URLSearchParams(path.substring(path.indexOf("?")));
+    if (params.has("query")) {
+      $(".search_form input[name=query]").val(params.get("query"));
       $(".describe_location").hide();
-    } else if (params.lat && params.lon) {
-      $(".search_form input[name=query]").val(params.lat + ", " + params.lon);
+    } else if (params.has("lat") && params.has("lon")) {
+      $(".search_form input[name=query]").val(params.get("lat") + ", " + params.get("lon"));
       $(".describe_location").hide();
     }
     OSM.loadSidebarContent(path, page.load);
index 10ce9471580b038c776c232c3a953c6d499c5559..6e42d4ac103e88f45259870aba389b104da82aad 100644 (file)
@@ -1,5 +1,3 @@
-//= require qs/dist/qs
-
 L.extend(L.LatLngBounds.prototype, {
   getSize: function () {
     return (this._northEast.lat - this._southWest.lat) *
@@ -152,17 +150,14 @@ L.OSM.Map = L.Map.extend({
   },
 
   getUrl: function (marker) {
-    var precision = OSM.zoomPrecision(this.getZoom()),
-        params = {};
+    const params = {};
 
     if (marker && this.hasLayer(marker)) {
-      var latLng = marker.getLatLng().wrap();
-      params.mlat = latLng.lat.toFixed(precision);
-      params.mlon = latLng.lng.toFixed(precision);
+      [params.mlat, params.mlon] = OSM.cropLocation(marker.getLatLng(), this.getZoom());
     }
 
     var url = window.location.protocol + "//" + OSM.SERVER_URL + "/",
-        query = Qs.stringify(params),
+        query = new URLSearchParams(params),
         hash = OSM.formatHash(this);
 
     if (query) url += "?" + query;
@@ -212,22 +207,22 @@ L.OSM.Map = L.Map.extend({
       return (interlaced_x << 1) | interlaced_y;
     }
 
-    var params = {};
+    const params = new URLSearchParams();
     var layers = this.getLayersCode().replace("M", "");
 
     if (layers) {
-      params.layers = layers;
+      params.set("layers", layers);
     }
 
     if (marker && this.hasLayer(marker)) {
-      params.m = "";
+      params.set("m", "");
     }
 
     if (this._object) {
-      params[this._object.type] = this._object.id;
+      params.set(this._object.type, this._object.id);
     }
 
-    var query = Qs.stringify(params);
+    const query = params.toString();
     if (query) {
       str += "?" + query;
     }
@@ -236,21 +231,14 @@ L.OSM.Map = L.Map.extend({
   },
 
   getGeoUri: function (marker) {
-    var precision = OSM.zoomPrecision(this.getZoom()),
-        latLng,
-        params = {};
+    let latLng = this.getCenter();
+    const zoom = this.getZoom();
 
     if (marker && this.hasLayer(marker)) {
-      latLng = marker.getLatLng().wrap();
-    } else {
-      latLng = this.getCenter();
+      latLng = marker.getLatLng();
     }
 
-    params.lat = latLng.lat.toFixed(precision);
-    params.lon = latLng.lng.toFixed(precision);
-    params.zoom = this.getZoom();
-
-    return "geo:" + params.lat + "," + params.lon + "?z=" + params.zoom;
+    return `geo:${OSM.cropLocation(latLng, zoom).join(",")}?z=${zoom}`;
   },
 
   addObject: function (object, callback) {
index 602d3af10afb65049f194af1dca778832686ec65..749cf5b7736767b6e52a59e70c02babac07d9022 100644 (file)
@@ -2,48 +2,46 @@
 //= depend_on settings.local.yml
 //= depend_on layers.yml
 //= depend_on key.yml
-//= require qs/dist/qs
 
 OSM = {
-<% if defined?(Settings.matomo) %>
-  MATOMO:                  <%= Settings.matomo.to_json %>,
-<% end %>
-
-  MAX_REQUEST_AREA:        <%= Settings.max_request_area.to_json %>,
-  SERVER_PROTOCOL:         <%= Settings.server_protocol.to_json %>,
-  SERVER_URL:              <%= Settings.server_url.to_json %>,
-  API_VERSION:             <%= Settings.api_version.to_json %>,
-  STATUS:                  <%= Settings.status.to_json %>,
-  MAX_NOTE_REQUEST_AREA:   <%= Settings.max_note_request_area.to_json %>,
-  OVERPASS_URL:            <%= Settings.overpass_url.to_json %>,
-  OVERPASS_CREDENTIALS:    <%= Settings.overpass_credentials.to_json %>,
-  NOMINATIM_URL:           <%= Settings.nominatim_url.to_json %>,
-  GRAPHHOPPER_URL:         <%= Settings.graphhopper_url.to_json %>,
-  FOSSGIS_OSRM_URL:        <%= Settings.fossgis_osrm_url.to_json %>,
-  FOSSGIS_VALHALLA_URL:    <%= Settings.fossgis_valhalla_url.to_json %>,
-  DEFAULT_LOCALE:          <%= I18n.default_locale.to_json %>,
-
-<% if Settings.key?(:thunderforest_key) %>
-  THUNDERFOREST_KEY:       <%= Settings.thunderforest_key.to_json %>,
-<% end %>
-
-<% if Settings.key?(:tracestrack_key) %>
-  TRACESTRACK_KEY:         <%= Settings.tracestrack_key.to_json %>,
-<% end %>
-
-  LAYER_DEFINITIONS:       <%= YAML.load_file(Rails.root.join("config/layers.yml")).to_json %>,
-  LAYERS_WITH_MAP_KEY:     <%= YAML.load_file(Rails.root.join("config/key.yml")).keys.to_json %>,
-
-  MARKER_GREEN:            <%= image_path("marker-green.png").to_json %>,
-  MARKER_RED:              <%= image_path("marker-red.png").to_json %>,
-
-  MARKER_ICON:             <%= image_path("leaflet/dist/images/marker-icon.png").to_json %>,
-  MARKER_ICON_2X:          <%= image_path("leaflet/dist/images/marker-icon-2x.png").to_json %>,
-  MARKER_SHADOW:           <%= image_path("leaflet/dist/images/marker-shadow.png").to_json %>,
-
-  NEW_NOTE_MARKER:         <%= image_path("new_note_marker.svg").to_json %>,
-  OPEN_NOTE_MARKER:        <%= image_path("open_note_marker.svg").to_json %>,
-  CLOSED_NOTE_MARKER:      <%= image_path("closed_note_marker.svg").to_json %>,
+  ...<%=
+    %i[
+      matomo
+      max_request_area
+      server_protocol
+      server_url
+      api_version
+      status
+      max_note_request_area
+      overpass_url
+      overpass_credentials
+      nominatim_url
+      graphhopper_url
+      fossgis_osrm_url
+      fossgis_valhalla_url
+      thunderforest_key
+      tracestrack_key
+    ]
+      .each_with_object({}) do |key, hash|
+        hash[key.to_s.upcase] = Settings.send(key) if Settings.respond_to?(key)
+      end.to_json
+  %>,
+
+  DEFAULT_LOCALE: <%= I18n.default_locale.to_json %>,
+
+  LAYER_DEFINITIONS: <%= YAML.load_file(Rails.root.join("config/layers.yml")).to_json %>,
+  LAYERS_WITH_MAP_KEY: <%= YAML.load_file(Rails.root.join("config/key.yml")).keys.to_json %>,
+
+  MARKER_GREEN: <%= image_path("marker-green.png").to_json %>,
+  MARKER_RED: <%= image_path("marker-red.png").to_json %>,
+
+  MARKER_ICON: <%= image_path("leaflet/dist/images/marker-icon.png").to_json %>,
+  MARKER_ICON_2X: <%= image_path("leaflet/dist/images/marker-icon-2x.png").to_json %>,
+  MARKER_SHADOW: <%= image_path("leaflet/dist/images/marker-shadow.png").to_json %>,
+
+  NEW_NOTE_MARKER: <%= image_path("new_note_marker.svg").to_json %>,
+  OPEN_NOTE_MARKER: <%= image_path("open_note_marker.svg").to_json %>,
+  CLOSED_NOTE_MARKER: <%= image_path("closed_note_marker.svg").to_json %>,
 
   apiUrl: function (object) {
     var apiType = object.type === "note" ? "notes" : object.type;
@@ -59,28 +57,12 @@ OSM = {
   },
 
   params: function (search) {
-    var params = {};
-
-    search = (search || window.location.search).replace("?", "").split(/&|;/);
-
-    for (var i = 0; i < search.length; ++i) {
-      var pair = search[i],
-          j = pair.indexOf("="),
-          key = pair.slice(0, j),
-          val = pair.slice(++j);
-
-      try {
-        params[key] = decodeURIComponent(val);
-      } catch (e) {
-        // Ignore parse exceptions
-      }
-    }
-
-    return params;
+    var query = search || window.location.search;
+    return Object.fromEntries(new URLSearchParams(query));
   },
 
   mapParams: function (search) {
-    var params = OSM.params(search), mapParams = {}, match;
+    var params = OSM.params(search), mapParams = {};
 
     if (params.mlon && params.mlat) {
       mapParams.marker = true;
@@ -158,9 +140,9 @@ OSM = {
       return args;
     }
 
-    hash = Qs.parse(hash.slice(i + 1));
+    const hashParams = new URLSearchParams(hash.slice(i + 1));
 
-    var map = (hash.map || "").split("/"),
+    var map = (hashParams.get("map") || "").split("/"),
         zoom = parseInt(map[0], 10),
         lat = parseFloat(map[1]),
         lon = parseFloat(map[2]);
@@ -170,8 +152,8 @@ OSM = {
       args.zoom = zoom;
     }
 
-    if (hash.layers) {
-      args.layers = hash.layers;
+    if (hashParams.has("layers")) {
+      args.layers = hashParams.get("layers");
     }
 
     return args;
@@ -190,13 +172,9 @@ OSM = {
       layers = args.layers || "";
     }
 
-    center = center.wrap();
     layers = layers.replace("M", "");
 
-    var precision = OSM.zoomPrecision(zoom),
-        hash = "#map=" + zoom +
-          "/" + center.lat.toFixed(precision) +
-          "/" + center.lng.toFixed(precision);
+    let hash = "#map=" + [zoom, ...OSM.cropLocation(center, zoom)].join("/");
 
     if (layers) {
       hash += "&layers=" + layers;
@@ -211,11 +189,16 @@ OSM = {
     return Math.ceil(Math.log10(pixels / degrees));
   },
 
+  cropLocation: function (latLng, zoom) {
+    const precision = OSM.zoomPrecision(zoom),
+          wrapped = latLng.wrap();
+    return [wrapped.lat, wrapped.lng].map(c => c.toFixed(precision));
+  },
+
   locationCookie: function (map) {
-    var center = map.getCenter().wrap(),
-        zoom = map.getZoom(),
-        precision = OSM.zoomPrecision(zoom);
-    return [center.lng.toFixed(precision), center.lat.toFixed(precision), zoom, map.getLayersCode()].join("|");
+    const zoom = map.getZoom(),
+          center = OSM.cropLocation(map.getCenter(), zoom).reverse();
+    return [...center, zoom, map.getLayersCode()].join("|");
   },
 
   distance: function (latlng1, latlng2) {
@@ -229,7 +212,7 @@ OSM = {
     return 6372795 * 2 * Math.asin(
       Math.sqrt(
         Math.pow(Math.sin(latdiff / 2), 2) +
-        Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(lngdiff / 2), 2)
+        (Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(lngdiff / 2), 2))
       ));
   }
 };
index b94db8b557e8fe7430be8e14fe490887fa107918..1d167d977fb70e85481179b0671e963c9bc1c08a 100644 (file)
@@ -64,12 +64,10 @@ $(document).ready(function () {
       map.on("click", function (e) {
         if (!$("#updatehome").is(":checked")) return;
 
-        var zoom = map.getZoom(),
-            precision = OSM.zoomPrecision(zoom),
-            location = e.latlng.wrap();
+        const [lat, lon] = OSM.cropLocation(e.latlng);
 
-        $("#home_lat").val(location.lat.toFixed(precision));
-        $("#home_lon").val(location.lng.toFixed(precision));
+        $("#home_lat").val(lat);
+        $("#home_lon").val(lon);
 
         deleted_lat = null;
         deleted_lon = null;
index 92b9f72409caebbe20fadaad52e55e5f26fc4ded..fc9af9803e53cbcdb340680b11ba95e9be3769e4 100644 (file)
@@ -819,7 +819,7 @@ tr.turn {
 /* Rules for the issues page */
 
 .issues.issues-index {
-  td.reporter_users {
+  td.reporting_users {
     max-width: 5rem;
   }
 }
index 2aa25fdd74611ecb2e3323af46ede63b8f72e5fe..439585462eba7e1e7f760183789eec960feffd12 100644 (file)
@@ -46,10 +46,7 @@ module Api
         way_ids = way_nodes.collect { |way_node| way_node.id[0] }
         ways = Way.preload(:way_nodes, :way_tags).find(way_ids)
 
-        list_of_way_nodes = ways.collect do |way|
-          way.way_nodes.collect(&:node_id)
-        end
-        list_of_way_nodes.flatten!
+        list_of_way_nodes = ways.flat_map { |way| way.way_nodes.map(&:node_id) }
       end
 
       # - [0] in case some thing links to node 0 which doesn't exist. Shouldn't actually ever happen but it does. FIXME: file a ticket for this
@@ -57,26 +54,12 @@ module Api
 
       nodes += Node.includes(:node_tags).find(nodes_to_fetch) unless nodes_to_fetch.empty?
 
-      visible_nodes = {}
-      @nodes = []
-      nodes.each do |node|
-        if node.visible?
-          visible_nodes[node.id] = node
-          @nodes << node
-        end
-      end
+      @nodes = nodes.filter(&:visible?)
 
-      @ways = []
-      way_ids = []
-      ways.each do |way|
-        if way.visible?
-          way_ids << way.id
-          @ways << way
-        end
-      end
+      @ways = ways.filter(&:visible?)
 
-      @relations = Relation.nodes(visible_nodes.keys).visible +
-                   Relation.ways(way_ids).visible
+      @relations = Relation.nodes(@nodes).visible +
+                   Relation.ways(@ways).visible
 
       # we do not normally return the "other" partners referenced by an relation,
       # e.g. if we return a way A that is referenced by relation X, and there's
index 2e9850aef060f2b6549de5cdecac599d95fbdd9a..0ba5032288b7c7b7ebb9803a00fc230506372775 100644 (file)
@@ -1,6 +1,14 @@
 module NoteHelper
   include ActionView::Helpers::TranslationHelper
 
+  def note_description(author, description)
+    if !author.nil? && author.status == "deleted"
+      RichText.new("text", t("notes.show.description_when_author_is_deleted"))
+    else
+      description
+    end
+  end
+
   def note_event(event, at, by)
     if by.nil?
       t("notes.show.event_#{event}_by_anonymous_html",
index da49347c3054a56f32a063e54cb44faf84bc2e79..13a9f9e9ea0c6be158bc8677bd6e7533ee96543a 100644 (file)
@@ -35,8 +35,8 @@ module SocialShareButtonHelper
 
   def generate_share_url(site, title, url)
     site = site.to_sym
-    title = URI.encode_www_form_component(title)
-    url = URI.encode_www_form_component(url)
+    title = URI.encode_uri_component(title)
+    url = URI.encode_uri_component(url)
 
     case site
     when :email
index 376516e9b4e8841a0ef9f132102d41ec2fd04c36..d37b863e53b5a77224375fc48990860144171cfe 100644 (file)
@@ -28,6 +28,8 @@
 class Note < ApplicationRecord
   include GeoRecord
 
+  belongs_to :author, :class_name => "User", :foreign_key => "user_id", :optional => true
+
   has_many :comments, -> { left_joins(:author).where(:visible => true, :users => { :status => [nil, "active", "confirmed"] }).order(:created_at) }, :class_name => "NoteComment", :foreign_key => :note_id
   has_many :all_comments, -> { left_joins(:author).order(:created_at) }, :class_name => "NoteComment", :foreign_key => :note_id, :inverse_of => :note
   has_many :subscriptions, :class_name => "NoteSubscription"
@@ -91,12 +93,20 @@ class Note < ApplicationRecord
 
   # Return the note's description, derived from the first comment
   def description
-    comments.first.body
+    if user_ip.nil? && user_id.nil?
+      comments.first.body
+    else
+      RichText.new("text", super)
+    end
   end
 
   # Return the note's author object, derived from the first comment
   def author
-    comments.first.author
+    if user_ip.nil? && user_id.nil?
+      comments.first.author
+    else
+      super
+    end
   end
 
   private
index fa70536f79227a5cdaa4a5954c6c5076022c6125..0bfdb385de41cafda4fbb1bfcb96f06b6bc13c92 100644 (file)
@@ -13,7 +13,7 @@ xml.item do
   xml.guid api_note_url(note)
   xml.description render(:partial => "description", :object => note, :formats => [:html])
 
-  xml.dc :creator, note.author.display_name if note.author
+  xml.dc :creator, note.author.display_name unless note.author.nil? || note.author.status == "deleted"
 
   xml.pubDate note.created_at.to_fs(:rfc822)
   xml.geo :lat, note.lat
index f828e5a4350b4a80bb64306190aa85ecc4cd18bd..d7005d75eb7571fcc1f6e2d20bc7f22d681c0115 100644 (file)
@@ -21,7 +21,8 @@
   <%= bootstrap_form_for @new_comment, :url => issue_comments_path(@issue) do |f| %>
     <%= f.richtext_field :body, :cols => 80, :rows => 20, :hide_label => true %>
     <%= f.form_group do %>
-      <%= f.check_box :reassign, { :label => t(".reassign_param"), :id => "reassign", :name => "reassign", :checked => false }, true, false %>
+      <%= f.check_box :reassign, { :label => @issue.assigned_role == "administrator" ? t(".reassign_to_moderators") : t(".reassign_to_administrators"),
+                                   :id => "reassign", :name => "reassign", :checked => false }, true, false %>
     <% end %>
     <%= f.primary %>
   <% end %>
index 6b291e58d082708362d28e32d05861c0d893c76f..9681704a1ddbdbbac23d8604b853cda6d1392928 100644 (file)
@@ -13,7 +13,7 @@
           <th><%= t ".reports" %></th>
           <th><%= t ".reported_item" %></th>
           <th><%= t ".reported_user" %></th>
-          <th class="reporter_users"><%= t ".reporter_users" %></th>
+          <th class="reporting_users"><%= t ".reporting_users" %></th>
           <th><%= t ".last_updated" %></th>
         </tr>
       </thead>
@@ -24,7 +24,7 @@
             <td class="text-nowrap"><%= link_to t(".reports_count", :count => issue.reports_count), issue %></td>
             <td><%= link_to reportable_title(issue.reportable), reportable_url(issue.reportable) %></td>
             <td><%= link_to issue.reported_user.display_name, issue.reported_user if issue.reported_user %></td>
-            <td class="reporter_users text-truncate">
+            <td class="reporting_users text-truncate">
               <% @unique_reporters[issue.id][:users].each do |reporter| %>
                 <%= link_to reporter.display_name, reporter, :class => "d-block text-truncate", :title => reporter.display_name %>
               <% end %>
index f805a10402b413aa295875010a051b7ecbbef76f..0b39234a93e61f2eaf222b0e221906f48bf8c567 100644 (file)
@@ -48,7 +48,7 @@
       </td>
       <td><%= link_to note.id, note %></td>
       <td><%= note_author(note.author) %></td>
-      <td><%= note.description.to_html %></td>
+      <td><%= note_description(note.author, note.description).to_html %></td>
       <td><%= friendly_date_ago(note.created_at) %></td>
       <td><%= friendly_date_ago(note.updated_at) %></td>
     </tr>
index a320240488e716224a8908bbed5b612988e7907e..3d9b4a9bace5aafa7d1a378abc6da3ed1a0bf54e 100644 (file)
@@ -5,7 +5,7 @@
 <div>
   <h4><%= t(".description") %></h4>
   <div class="overflow-hidden ms-2">
-    <%= h(@note.description.to_html) %>
+    <%= h(note_description(@note.author, @note.description).to_html) %>
   </div>
 
   <div class="details" data-coordinates="<%= @note.lat %>,<%= @note.lon %>" data-status="<%= @note.status %>">
index b4d0429e4ae8417db3fbfe4b3324fe721e20fc39..7cbb9575f39a468295bca251f76b33d4a4574f5d 100644 (file)
@@ -1,9 +1,15 @@
 const globals = require("globals");
 const js = require("@eslint/js");
+const erb = require("eslint-plugin-erb");
+const stylisticJs = require("@stylistic/eslint-plugin-js");
 
 module.exports = [
   js.configs.recommended,
+  erb.configs.recommended,
   {
+    plugins: {
+      "@stylistic": stylisticJs
+    },
     languageOptions: {
       ecmaVersion: 2021,
       sourceType: "script",
@@ -15,38 +21,71 @@ module.exports = [
         L: "readonly",
         OSM: "writable",
         Matomo: "readonly",
-        Qs: "readonly",
         Turbo: "readonly",
         updateLinks: "readonly"
       }
     },
+    linterOptions: {
+      // The "unused disable directive" is set to "warn" by default.
+      // For the ERB plugin to work correctly, you must disable
+      // this directive to avoid issues described here
+      // https://github.com/eslint/eslint/discussions/18114
+      // If you're using the CLI, you might also use the following flag:
+      // --report-unused-disable-directives-severity=off
+      reportUnusedDisableDirectives: "off"
+    },
     rules: {
-      "accessor-pairs": "error",
-      "array-bracket-newline": ["error", "consistent"],
-      "array-bracket-spacing": "error",
-      "array-callback-return": "error",
-      "block-scoped-var": "error",
-      "block-spacing": "error",
-      "brace-style": ["error", "1tbs", { allowSingleLine: true }],
-      "comma-dangle": "error",
-      "comma-spacing": "error",
-      "comma-style": "error",
-      "computed-property-spacing": "error",
-      "curly": ["error", "multi-line", "consistent"],
-      "dot-location": ["error", "property"],
-      "dot-notation": "error",
-      "eol-last": "error",
-      "eqeqeq": ["error", "smart"],
-      "func-call-spacing": "error",
-      "indent": ["error", 2, {
+      "@stylistic/array-bracket-newline": ["error", "consistent"],
+      "@stylistic/array-bracket-spacing": "error",
+      "@stylistic/block-spacing": "error",
+      "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }],
+      "@stylistic/comma-dangle": "error",
+      "@stylistic/comma-spacing": "error",
+      "@stylistic/comma-style": "error",
+      "@stylistic/computed-property-spacing": "error",
+      "@stylistic/dot-location": ["error", "property"],
+      "@stylistic/eol-last": "error",
+      "@stylistic/func-call-spacing": "error",
+      "@stylistic/indent": ["error", 2, {
         SwitchCase: 1,
         VariableDeclarator: "first",
         FunctionDeclaration: { parameters: "first" },
         FunctionExpression: { parameters: "first" },
         CallExpression: { arguments: "first" }
       }],
-      "key-spacing": "error",
-      "keyword-spacing": "error",
+      "@stylistic/key-spacing": "error",
+      "@stylistic/keyword-spacing": "error",
+      "@stylistic/no-floating-decimal": "error",
+      "@stylistic/no-mixed-operators": "error",
+      "@stylistic/no-multiple-empty-lines": "error",
+      "@stylistic/no-multi-spaces": "error",
+      "@stylistic/no-trailing-spaces": "error",
+      "@stylistic/no-whitespace-before-property": "error",
+      "@stylistic/object-curly-newline": ["error", { consistent: true }],
+      "@stylistic/object-curly-spacing": ["error", "always"],
+      "@stylistic/object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
+      "@stylistic/operator-linebreak": ["error", "after"],
+      "@stylistic/padded-blocks": ["error", "never"],
+      "@stylistic/quote-props": ["error", "consistent-as-needed", { keywords: true, numbers: true }],
+      "@stylistic/quotes": ["error", "double"],
+      "@stylistic/semi": ["error", "always"],
+      "@stylistic/semi-spacing": "error",
+      "@stylistic/semi-style": "error",
+      "@stylistic/space-before-blocks": "error",
+      "@stylistic/space-before-function-paren": ["error", { named: "never" }],
+      "@stylistic/space-in-parens": "error",
+      "@stylistic/space-infix-ops": "error",
+      "@stylistic/space-unary-ops": "error",
+      "@stylistic/switch-colon-spacing": "error",
+      "@stylistic/wrap-iife": "error",
+      "@stylistic/wrap-regex": "error",
+
+      "accessor-pairs": "error",
+      "array-callback-return": "error",
+      "block-scoped-var": "error",
+      "curly": ["error", "multi-line", "consistent"],
+      "dot-notation": "error",
+      "eqeqeq": ["error", "smart"],
       "no-alert": "warn",
       "no-array-constructor": "error",
       "no-caller": "error",
@@ -57,7 +96,6 @@ module.exports = [
       "no-extend-native": "error",
       "no-extra-bind": "error",
       "no-extra-label": "error",
-      "no-floating-decimal": "error",
       "no-implicit-coercion": "warn",
       "no-implicit-globals": "warn",
       "no-implied-eval": "error",
@@ -68,9 +106,6 @@ module.exports = [
       "no-lone-blocks": "error",
       "no-lonely-if": "error",
       "no-loop-func": "error",
-      "no-mixed-operators": "error",
-      "no-multiple-empty-lines": "error",
-      "no-multi-spaces": "error",
       "no-multi-str": "error",
       "no-negated-condition": "error",
       "no-nested-ternary": "error",
@@ -86,7 +121,6 @@ module.exports = [
       "no-self-compare": "error",
       "no-sequences": "error",
       "no-throw-literal": "error",
-      "no-trailing-spaces": "error",
       "no-undef-init": "error",
       "no-undefined": "error",
       "no-unmodified-loop-condition": "error",
@@ -99,26 +133,7 @@ module.exports = [
       "no-use-before-define": ["error", { functions: false }],
       "no-void": "error",
       "no-warning-comments": "warn",
-      "no-whitespace-before-property": "error",
-      "object-curly-newline": ["error", { consistent: true }],
-      "object-curly-spacing": ["error", "always"],
-      "object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
-      "operator-linebreak": ["error", "after"],
-      "padded-blocks": ["error", "never"],
-      "quote-props": ["error", "consistent-as-needed", { keywords: true, numbers: true }],
-      "quotes": ["error", "double"],
       "radix": ["error", "always"],
-      "semi": ["error", "always"],
-      "semi-spacing": "error",
-      "semi-style": "error",
-      "space-before-blocks": "error",
-      "space-before-function-paren": ["error", { named: "never" }],
-      "space-in-parens": "error",
-      "space-infix-ops": "error",
-      "space-unary-ops": "error",
-      "switch-colon-spacing": "error",
-      "wrap-iife": "error",
-      "wrap-regex": "error",
       "yoda": "error"
     }
   },
index 172c6618232aedb1927e280e0cc55cd504e0aa18..25794cc9fde873a5d041605d5f2db5efa7bf005a 100644 (file)
@@ -1512,7 +1512,7 @@ en:
       reports: Reports
       last_updated: Last Updated
       last_updated_time_ago_user_html: "%{time_ago} by %{user}"
-      reporter_users: Reporter Users
+      reporting_users: Reporting Users
       reports_count:
         one: "%{count} Report"
         other: "%{count} Reports"
@@ -1551,7 +1551,8 @@ en:
       reopened: Issue status has been set to 'Open'
     comments:
       comment_from_html: "Comment from %{user_link} on %{comment_created_at}"
-      reassign_param: Reassign Issue?
+      reassign_to_moderators: Reassign Issue to Moderators
+      reassign_to_administrators: Reassign Issue to Administrators
     reports:
       reported_by_html: "Reported as %{category} by %{user} on %{updated_at}"
     helper:
@@ -3069,6 +3070,7 @@ en:
       open_title: "Unresolved note #%{note_name}"
       closed_title: "Resolved note #%{note_name}"
       hidden_title: "Hidden note #%{note_name}"
+      description_when_author_is_deleted: "deleted"
       event_opened_by_html: "Created by %{user} %{time_ago}"
       event_opened_by_anonymous_html: "Created by anonymous %{time_ago}"
       event_commented_by_html: "Comment from %{user} %{time_ago}"
index 790ef150d6e7b3220921348ee373857eb86356de..27b550e40bd1f00ff0be62f75c43d5c67f4d08a2 100644 (file)
@@ -10,7 +10,7 @@ end
 
 def js_files
   Rails.application.assets.each_file.select do |file|
-    file.ends_with?(".js") && !file.match?(%r{/(gems|vendor|i18n|node_modules)/})
+    (file.ends_with?(".js") || file.ends_with?(".js.erb")) && !file.match?(%r{/(gems|vendor|i18n|node_modules)/})
   end
 end
 
index 0edf11d8deadfe40875835230a75bad3fc8a1566..371d01aff40a1c24a000a65e0e906f498df93fcb 100644 (file)
@@ -6,11 +6,12 @@
     "js-cookie": "^3.0.0",
     "leaflet": "^1.8.0",
     "leaflet.locatecontrol": "^0.83.0",
-    "osm-community-index": "^5.2.0",
-    "qs": "^6.9.4"
+    "osm-community-index": "^5.2.0"
   },
   "devDependencies": {
     "eslint": "^9.0.0",
+    "eslint-plugin-erb": "^2.1.0",
+    "@stylistic/eslint-plugin-js": "^3.0.0",
     "eslint-formatter-compact": "^8.40.0"
   }
 }
index d9afb94a21236216d2abdabdb6105aa7142fdcfc..39b1f3cf81d26773d27c3a9d4dd8183b615bb8a9 100644 (file)
@@ -153,17 +153,19 @@ module Api
       get changeset_show_path(changeset)
       assert_response :success, "cannot get first changeset"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 0
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset do
+        assert_dom "> discussion", 0
+      end
 
       get changeset_show_path(changeset), :params => { :include_discussion => true }
       assert_response :success, "cannot get first changeset with comments"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 1
-      assert_select "osm>changeset>discussion>comment", 0
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset do
+        assert_dom "> discussion", 1
+        assert_dom "> discussion > comment", 0
+      end
     end
 
     def test_show_comments
@@ -174,16 +176,20 @@ module Api
       get changeset_show_path(changeset), :params => { :include_discussion => true }
       assert_response :success, "cannot get closed changeset with comments"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 1
-      assert_select "osm>changeset>discussion>comment", 3
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment2.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
-      assert_select "osm>changeset>discussion>comment:nth-child(3)>@id", comment3.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(3)>@visible", "true"
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1 do
+        assert_single_changeset changeset do
+          assert_dom "> discussion", 1 do
+            assert_dom "> comment", 3 do |dom_comments|
+              assert_dom dom_comments[0], "> @id", comment1.id.to_s
+              assert_dom dom_comments[0], "> @visible", "true"
+              assert_dom dom_comments[1], "> @id", comment2.id.to_s
+              assert_dom dom_comments[1], "> @visible", "true"
+              assert_dom dom_comments[2], "> @id", comment3.id.to_s
+              assert_dom dom_comments[2], "> @visible", "true"
+            end
+          end
+        end
+      end
 
       # one hidden comment not included because not asked for
       comment2.update(:visible => false)
@@ -191,28 +197,34 @@ module Api
       get changeset_show_path(changeset), :params => { :include_discussion => true }
       assert_response :success, "cannot get closed changeset with comments"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 1
-      assert_select "osm>changeset>discussion>comment", 2
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment3.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset do
+        assert_dom "> discussion", 1 do
+          assert_dom "> comment", 2 do |dom_comments|
+            assert_dom dom_comments[0], "> @id", comment1.id.to_s
+            assert_dom dom_comments[0], "> @visible", "true"
+            assert_dom dom_comments[1], "> @id", comment3.id.to_s
+            assert_dom dom_comments[1], "> @visible", "true"
+          end
+        end
+      end
 
       # one hidden comment not included because no permissions
       get changeset_show_path(changeset), :params => { :include_discussion => true, :show_hidden_comments => true }
       assert_response :success, "cannot get closed changeset with comments"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 1
-      assert_select "osm>changeset>discussion>comment", 2
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
-      # maybe will show an empty comment element with visible=false in the future
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment3.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "true"
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset do
+        assert_dom "> discussion", 1 do
+          assert_dom "> comment", 2 do |dom_comments|
+            assert_dom dom_comments[0], "> @id", comment1.id.to_s
+            assert_dom dom_comments[0], "> @visible", "true"
+            # maybe will show an empty comment element with visible=false in the future
+            assert_dom dom_comments[1], "> @id", comment3.id.to_s
+            assert_dom dom_comments[1], "> @visible", "true"
+          end
+        end
+      end
 
       # one hidden comment shown to moderators
       moderator_user = create(:moderator_user)
@@ -221,16 +233,19 @@ module Api
                                           :headers => auth_header
       assert_response :success, "cannot get closed changeset with comments"
 
-      assert_select "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
-      assert_single_changeset changeset
-      assert_select "osm>changeset>discussion", 1
-      assert_select "osm>changeset>discussion>comment", 3
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@id", comment1.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(1)>@visible", "true"
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@id", comment2.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(2)>@visible", "false"
-      assert_select "osm>changeset>discussion>comment:nth-child(3)>@id", comment3.id.to_s
-      assert_select "osm>changeset>discussion>comment:nth-child(3)>@visible", "true"
+      assert_dom "osm[version='#{Settings.api_version}'][generator='#{Settings.generator}']", 1
+      assert_single_changeset changeset do
+        assert_dom "> discussion", 1 do
+          assert_dom "> comment", 3 do |dom_comments|
+            assert_dom dom_comments[0], "> @id", comment1.id.to_s
+            assert_dom dom_comments[0], "> @visible", "true"
+            assert_dom dom_comments[1], "> @id", comment2.id.to_s
+            assert_dom dom_comments[1], "> @visible", "false"
+            assert_dom dom_comments[2], "> @id", comment3.id.to_s
+            assert_dom dom_comments[2], "> @visible", "true"
+          end
+        end
+      end
     end
 
     def test_show_json
@@ -2657,16 +2672,18 @@ module Api
 
     ##
     # check that the output consists of one specific changeset
-    def assert_single_changeset(changeset)
-      assert_select "osm>changeset", 1
-      assert_select "osm>changeset>@id", changeset.id.to_s
-      assert_select "osm>changeset>@created_at", changeset.created_at.xmlschema
-      if changeset.open?
-        assert_select "osm>changeset>@open", "true"
-        assert_select "osm>changeset>@closed_at", 0
-      else
-        assert_select "osm>changeset>@open", "false"
-        assert_select "osm>changeset>@closed_at", changeset.closed_at.xmlschema
+    def assert_single_changeset(changeset, &)
+      assert_dom "> changeset", 1 do
+        assert_dom "> @id", changeset.id.to_s
+        assert_dom "> @created_at", changeset.created_at.xmlschema
+        if changeset.open?
+          assert_dom "> @open", "true"
+          assert_dom "> @closed_at", 0
+        else
+          assert_dom "> @open", "false"
+          assert_dom "> @closed_at", changeset.closed_at.xmlschema
+        end
+        yield if block_given?
       end
     end
 
index 136298381ae8f094f94ecb3d44b7a3a016690257..0634632b850c5209ee759623813c5dd4ab718801 100644 (file)
@@ -9,9 +9,14 @@ class SocialShareButtonHelperTest < ActionView::TestCase
 
     SOCIAL_SHARE_CONFIG.each_value do |icon|
       assert_dom buttons_dom, "div:has(a img[src='/images/#{icon}'])", :count => 1 do
-        assert_dom "a[href*='Test+Title']"
+        assert_dom "a[href*='Test%20Title']"
         assert_dom "a[href*='https%3A%2F%2Fexample.com']"
       end
     end
   end
+
+  def test_generate_share_url_email
+    url = generate_share_url(:email, "Diary Entry Title", "https://osm.example.com/some/diary/entry")
+    assert_equal "mailto:?subject=Diary%20Entry%20Title&body=https%3A%2F%2Fosm.example.com%2Fsome%2Fdiary%2Fentry", url
+  end
 end
index 0450943149ed9693bb4588996fb726015eabc906..a72ca21107f9be065f48dd6bde0c7ad3779f5de4 100644 (file)
@@ -5,7 +5,6 @@
 //= require leaflet.osm
 //= require leaflet.map
 //= require i18n/translations
-//= require qs/dist/qs
 
 describe("OSM", function () {
   describe(".apiUrl", function () {
index dd13a286f55e3061be1c51bbba764fcc24628ce5..410d5dfb7b0fdcaa0e0a42486445a2117cde4044 100644 (file)
@@ -23,7 +23,7 @@ class CreateNoteTest < ApplicationSystemTestCase
       fill_in "text", :with => "Some newly added note description"
       click_on "Add Note"
 
-      assert_content "Unresolved note ##{Note.last.id}"
+      assert_content "Unresolved note #"
       assert_content "Some newly added note description"
     end
   end
@@ -56,7 +56,7 @@ class CreateNoteTest < ApplicationSystemTestCase
 
       click_on "Add Note"
 
-      assert_content "Unresolved note ##{Note.last.id}"
+      assert_content "Unresolved note #"
       assert_content "Some newly added note description"
     end
   end
index a961ea80f7a54099597920e2073c0055a573cad9..e26ae89ac6556cbec289f25e52ed445ac98e33b6 100644 (file)
@@ -114,15 +114,15 @@ class IssuesTest < ApplicationSystemTestCase
     assert_equal("test comment", issue.comments.first.body)
   end
 
-  def test_reassign_issue
-    issue = create(:issue)
-    assert_equal "administrator", issue.assigned_role
+  def test_reassign_issue_to_moderators
+    issue = create(:issue, :assigned_role => "administrator")
     sign_in_as(create(:administrator_user))
 
     visit issue_path(issue)
 
+    assert_unchecked_field "Reassign Issue to Moderators"
     fill_in :issue_comment_body, :with => "reassigning to moderators"
-    check :reassign
+    check "Reassign Issue to Moderators"
     click_on "Add Comment"
 
     assert_content "and the issue was reassigned"
@@ -132,6 +132,24 @@ class IssuesTest < ApplicationSystemTestCase
     assert_equal "moderator", issue.assigned_role
   end
 
+  def test_reassign_issue_to_administrators
+    issue = create(:issue, :assigned_role => "moderator")
+    sign_in_as(create(:moderator_user))
+
+    visit issue_path(issue)
+
+    assert_unchecked_field "Reassign Issue to Administrators"
+    fill_in :issue_comment_body, :with => "reassigning to administrators"
+    check "Reassign Issue to Administrators"
+    click_on "Add Comment"
+
+    assert_content "and the issue was reassigned"
+    assert_current_path issues_path(:status => "open")
+
+    issue.reload
+    assert_equal "administrator", issue.assigned_role
+  end
+
   def test_reassign_issue_as_super_user
     issue = create(:issue)
     sign_in_as(create(:super_user))
@@ -139,7 +157,7 @@ class IssuesTest < ApplicationSystemTestCase
     visit issue_path(issue)
 
     fill_in :issue_comment_body, :with => "reassigning to moderators"
-    check :reassign
+    check "Reassign Issue to Moderators"
     click_on "Add Comment"
 
     assert_content "and the issue was reassigned"
index 55bc0851ba9256f34b00f0c5e9a7f67ab6576739..c15c97e29b3c446b7835d4e34d130e056b94bc7b 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b"
   integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==
 
+"@stylistic/eslint-plugin-js@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-3.0.1.tgz#15638c55a9adab2c110243a9f0d812264b067aab"
+  integrity sha512-hjp6BKXSUdlY4l20pDb0EjIB5PtQDGihk2EUKCjJ5gaRVfcmMMkaIyVd/yK3oH7OLxWWBxJ8qSSo+zEdkmpnYA==
+  dependencies:
+    eslint-visitor-keys "^4.2.0"
+    espree "^10.3.0"
+
 "@types/estree@^1.0.6":
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
@@ -151,22 +159,6 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-call-bind-apply-helpers@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840"
-  integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==
-  dependencies:
-    es-errors "^1.3.0"
-    function-bind "^1.1.2"
-
-call-bound@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681"
-  integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==
-  dependencies:
-    call-bind-apply-helpers "^1.0.1"
-    get-intrinsic "^1.2.6"
-
 callsites@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -223,32 +215,6 @@ diacritics@^1.3.0:
   resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
   integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==
 
-dunder-proto@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
-  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
-  dependencies:
-    call-bind-apply-helpers "^1.0.1"
-    es-errors "^1.3.0"
-    gopd "^1.2.0"
-
-es-define-property@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
-  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
-
-es-errors@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
-  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
-
-es-object-atoms@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.0.tgz#095de9ecceeb2ca79668212b60ead450ffd323bf"
-  integrity sha512-Ujz8Al/KfOVR7fkaghAB1WvnLsdYxHDWmfoi2vlA2jZWRg31XhIC1a4B+/I24muD8iSbHxJ1JkrfqmWb65P/Mw==
-  dependencies:
-    es-errors "^1.3.0"
-
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -259,6 +225,11 @@ eslint-formatter-compact@^8.40.0:
   resolved "https://registry.yarnpkg.com/eslint-formatter-compact/-/eslint-formatter-compact-8.40.0.tgz#d7455b2d75fd70e8c0e7a98a5e189f168e9dfe2d"
   integrity sha512-cwGUs113TgmTQXecx5kfRjB7m0y2wkDLSadPTE2pK6M/wO4N8PjmUaoWOFNCP9MHgsiZwgqd5bZFnDCnszC56Q==
 
+eslint-plugin-erb@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-erb/-/eslint-plugin-erb-2.1.1.tgz#8a0a6c2bcaf3a8573c381b595969145aff93cfc6"
+  integrity sha512-AhznaVwRpQqR8NADjN4SZnKNbaIdAbGxTjCg6cj3UhwGyQOUJ6kXwhYrl1LYrGDNx7Ouyd8xuEG7wepFZyPgFw==
+
 eslint-scope@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442"
@@ -393,35 +364,6 @@ flatted@^3.2.9:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
   integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
 
-function-bind@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
-  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
-
-get-intrinsic@^1.2.5, get-intrinsic@^1.2.6:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
-  integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
-  dependencies:
-    call-bind-apply-helpers "^1.0.1"
-    es-define-property "^1.0.1"
-    es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
-    function-bind "^1.1.2"
-    get-proto "^1.0.0"
-    gopd "^1.2.0"
-    has-symbols "^1.1.0"
-    hasown "^2.0.2"
-    math-intrinsics "^1.1.0"
-
-get-proto@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
-  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
-  dependencies:
-    dunder-proto "^1.0.1"
-    es-object-atoms "^1.0.0"
-
 glob-parent@^6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
@@ -434,28 +376,11 @@ globals@^14.0.0:
   resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
   integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
 
-gopd@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
-  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
-
 has-flag@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
-  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
-
-hasown@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
-  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
-  dependencies:
-    function-bind "^1.1.2"
-
 ignore@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
@@ -560,11 +485,6 @@ lodash.merge@^4.6.2:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-math-intrinsics@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
-  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
-
 minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -582,11 +502,6 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
 
-object-inspect@^1.13.3:
-  version "1.13.3"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a"
-  integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==
-
 optionator@^0.9.3:
   version "0.9.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
@@ -647,13 +562,6 @@ punycode@^2.1.0:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-qs@^6.9.4:
-  version "6.14.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
-  integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
-  dependencies:
-    side-channel "^1.1.0"
-
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -671,46 +579,6 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-side-channel-list@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
-  integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
-  dependencies:
-    es-errors "^1.3.0"
-    object-inspect "^1.13.3"
-
-side-channel-map@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
-  integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
-  dependencies:
-    call-bound "^1.0.2"
-    es-errors "^1.3.0"
-    get-intrinsic "^1.2.5"
-    object-inspect "^1.13.3"
-
-side-channel-weakmap@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
-  integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
-  dependencies:
-    call-bound "^1.0.2"
-    es-errors "^1.3.0"
-    get-intrinsic "^1.2.5"
-    object-inspect "^1.13.3"
-    side-channel-map "^1.0.1"
-
-side-channel@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
-  integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
-  dependencies:
-    es-errors "^1.3.0"
-    object-inspect "^1.13.3"
-    side-channel-list "^1.0.0"
-    side-channel-map "^1.0.1"
-    side-channel-weakmap "^1.0.2"
-
 strip-json-comments@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"