diff --git a/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js b/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js
index 498ca51fe..24e0a30e1 100644
--- a/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js
+++ b/adhocracy4/maps_react/static/a4maps_react/AddMarkerControl.js
@@ -19,7 +19,7 @@ export function checkPointInsidePolygon (marker, polygons) {
const markerProps = { icon: makeIcon(), draggable: true }
-class AddMarkerControlClass extends L.Control {
+export class AddMarkerControlClass extends L.Control {
constructor ({ input, point }) {
super()
this.marker = null
@@ -36,7 +36,7 @@ class AddMarkerControlClass extends L.Control {
}
updateMarker (latlng) {
- const isInsideConstraints = checkPointInsidePolygon(latlng, this.map.constraints)
+ const isInsideConstraints = checkPointInsidePolygon(latlng, this.map.markerConstraints)
if (isInsideConstraints) {
this.oldCoords = latlng
if (this.marker) {
@@ -51,11 +51,11 @@ class AddMarkerControlClass extends L.Control {
onDragend (e) {
const targetPosition = e.target.getLatLng()
- const isInsideConstraints = checkPointInsidePolygon(targetPosition, this.map.constraints)
+ const isInsideConstraints = checkPointInsidePolygon(targetPosition, this.map.markerConstraints)
if (!isInsideConstraints) {
e.target.setLatLng(this.oldCoords)
} else {
- this.oldCoords = targetPosition
+ this.updateMarker(targetPosition)
}
}
diff --git a/adhocracy4/maps_react/static/a4maps_react/Map.jsx b/adhocracy4/maps_react/static/a4maps_react/Map.jsx
index 067cea639..5a6092826 100644
--- a/adhocracy4/maps_react/static/a4maps_react/Map.jsx
+++ b/adhocracy4/maps_react/static/a4maps_react/Map.jsx
@@ -1,7 +1,6 @@
import React, { useRef, useImperativeHandle } from 'react'
import { MapContainer, GeoJSON } from 'react-leaflet'
import MaplibreGlLayer from './MaplibreGlLayer'
-import ZoomControl from './ZoomControl'
const polygonStyle = {
color: '#0076ae',
@@ -10,7 +9,7 @@ const polygonStyle = {
fillOpacity: 0.2
}
-export const Map = React.forwardRef(function Map (
+const Map = React.forwardRef(function Map (
{ attribution, baseUrl, polygon, omtToken, children, ...rest }, ref
) {
const map = useRef()
@@ -21,8 +20,9 @@ export const Map = React.forwardRef(function Map (
return
}
map.current.fitBounds(polygon.getBounds())
- map.current.options.minZoom = map.current.getZoom()
- map.current.constraints = polygon
+ map.current.setMinZoom(map.current.getZoom())
+ // used in AddMarkerControl to specify where markers can be placed
+ map.current.markerConstraints = polygon
}
return (
@@ -30,14 +30,14 @@ export const Map = React.forwardRef(function Map (
style={{ minHeight: 300 }}
zoom={13}
maxZoom={18}
- zoomControl={false}
{...rest}
ref={map}
>
{polygon && }
-
{children}
)
})
+
+export default Map
diff --git a/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js b/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js
deleted file mode 100644
index cb308dd71..000000000
--- a/adhocracy4/maps_react/static/a4maps_react/ZoomControl.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { createControlComponent } from '@react-leaflet/core'
-import L from 'leaflet'
-
-// Create a Leaflet Control
-const createLeafletElement = (props) => {
- const zoomControl = L.control.zoom(props)
-
- const updateDisabled = () => {
- const map = zoomControl._map
- if (!map) {
- return
- }
-
- const className = 'leaflet-disabled'
- const zoomInBtn = zoomControl._zoomInButton
- const zoomOutBtn = zoomControl._zoomOutButton
-
- // disable button when at min/max zoom
- if (map._zoom === map.getMinZoom()) {
- L.DomUtil.addClass(zoomOutBtn, className)
- } else {
- L.DomUtil.removeClass(zoomOutBtn, className)
- }
-
- if (map._zoom === map.getMaxZoom() || (map._zoomSnap && Math.abs(map.getZoom() - map.getMaxZoom()) < map._zoomSnap)) {
- L.DomUtil.addClass(zoomInBtn, className)
- } else {
- L.DomUtil.removeClass(zoomInBtn, className)
- }
- }
-
- zoomControl.onAdd = (map) => {
- const container = L.Control.Zoom.prototype.onAdd.call(zoomControl, map)
- map.on('zoom', updateDisabled)
- updateDisabled()
- return container
- }
-
- zoomControl.onRemove = (map) => {
- map.off('zoom', updateDisabled)
- L.Control.Zoom.prototype.onRemove.call(zoomControl, map)
- }
-
- return zoomControl
-}
-
-const ZoomControl = createControlComponent(createLeafletElement)
-export default ZoomControl
diff --git a/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js b/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js
new file mode 100644
index 000000000..1a3400263
--- /dev/null
+++ b/adhocracy4/maps_react/static/a4maps_react/__tests__/AddMarkerControl.jest.js
@@ -0,0 +1,60 @@
+import L from 'leaflet'
+import { AddMarkerControlClass } from '../AddMarkerControl'
+import { polygonData } from './Map.jest'
+import { jest } from '@jest/globals'
+
+describe('AddMarkerControlClass', () => {
+ const polygonGeoJSON = L.geoJSON(polygonData)
+ const map = { on: jest.fn(), off: jest.fn(), addLayer: jest.fn(), markerConstraints: polygonGeoJSON }
+ const point = JSON.stringify({
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'Point',
+ coordinates: [5, 5]
+ }
+ })
+
+ it('sets a marker', () => {
+ const input = document.createElement('input')
+ const instance = new AddMarkerControlClass({ input })
+ instance.map = map
+
+ const latlng = { lat: 10, lng: 5 }
+
+ expect(instance.marker).toBe(null)
+ instance.updateMarker(latlng)
+ expect(instance.marker).toBeDefined()
+ expect(input.value).toEqual(expect.stringContaining('5,10'))
+ instance.updateMarker({ lat: 2, lng: 5 })
+ expect(input.value).toEqual(expect.stringContaining('5,2'))
+ })
+
+ it('does not set a marker when outside', () => {
+ const input = document.createElement('input')
+ const instance = new AddMarkerControlClass({ input })
+ instance.map = map
+ const latlng = { lat: 15, lng: 15 }
+ expect(instance.marker).toBe(null)
+ instance.updateMarker(latlng)
+ expect(instance.marker).toBe(null)
+ expect(input.value).toEqual('')
+ })
+
+ it('updates on drag', () => {
+ const input = document.createElement('input')
+ const instance = new AddMarkerControlClass({ input, point })
+ instance.map = map
+ expect(instance.oldCoords).toStrictEqual([5, 5])
+ const newCoords = { lat: 10, lng: 10 }
+
+ const e = { target: { getLatLng: () => newCoords, setLatLng: jest.fn() } }
+ instance.onDragend(e)
+ expect(instance.oldCoords).toStrictEqual(newCoords)
+
+ const e2 = { target: { getLatLng: () => ({ lat: 15, lng: 15 }), setLatLng: jest.fn() } }
+ instance.onDragend(e2)
+ expect(e2.target.setLatLng).toHaveBeenCalledWith(newCoords)
+ expect(instance.oldCoords).toStrictEqual(newCoords)
+ })
+})
diff --git a/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx b/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx
new file mode 100644
index 000000000..278f07b1d
--- /dev/null
+++ b/adhocracy4/maps_react/static/a4maps_react/__tests__/Map.jest.jsx
@@ -0,0 +1,66 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import Map from '../Map'
+
+export const polygonData = {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'Polygon',
+ coordinates: [
+ [
+ [0, 0],
+ [10, 0],
+ [10, 10],
+ [0, 10]
+ ]
+ ]
+ }
+}
+
+jest.mock('react-leaflet', () => {
+ const ActualReactLeaflet = jest.requireActual('react-leaflet')
+ const React = require('react')
+
+ const MapContainer = React.forwardRef((props, ref) => (
+
+ ))
+ MapContainer.displayName = 'MapContainer'
+
+ const GeoJSON = React.forwardRef((props, ref) => (
+
+ ))
+ GeoJSON.displayName = 'GeoJSON'
+
+ return {
+ __esModule: true,
+ ...ActualReactLeaflet,
+ GeoJSON,
+ MapContainer
+ }
+})
+
+describe('Map component tests', () => {
+ test('component renders', () => {
+ render()
+ const mapNode = screen.getByTestId('map')
+
+ expect(mapNode).toBeTruthy()
+ })
+
+ test('renders map with GeoJSON when polygon prop is provided', () => {
+ render()
+ const geoJsonNode = screen.getByTestId('geojson')
+
+ expect(geoJsonNode).toBeTruthy()
+ })
+
+ test('does not render GeoJSON when polygon prop is not provided', () => {
+ render()
+ const geoJsonNode = screen.queryByTestId('geojson')
+
+ expect(geoJsonNode).toBeFalsy()
+ })
+})
diff --git a/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html b/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html
index ae9176399..b971118ad 100644
--- a/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html
+++ b/adhocracy4/maps_react/templates/a4maps_react/map_choose_point_widget.html
@@ -1,4 +1,4 @@
{% load react_maps_tags %}
-{% react_choose_point polygon=polygon point=point name=name %}
+{% react_choose_point polygon point name %}
diff --git a/adhocracy4/maps_react/templatetags/react_maps_tags.py b/adhocracy4/maps_react/templatetags/react_maps_tags.py
index 4f5648cc2..c8a4f84b6 100644
--- a/adhocracy4/maps_react/templatetags/react_maps_tags.py
+++ b/adhocracy4/maps_react/templatetags/react_maps_tags.py
@@ -1,11 +1,20 @@
+import json
+
from django import template
+from django.utils.html import format_html
-from adhocracy4.maps_react.utils import react_tag_factory
+from adhocracy4.maps_react.utils import get_map_settings
register = template.Library()
-register.simple_tag(
- react_tag_factory("choose-point"),
- False,
- "react_choose_point",
-)
+
+@register.simple_tag()
+def react_choose_point(polygon, point, name):
+ attributes = {
+ "map": get_map_settings(polygon=polygon, point=point, name=name),
+ }
+
+ return format_html(
+ '',
+ attributes=json.dumps(attributes),
+ )
diff --git a/adhocracy4/maps_react/utils.py b/adhocracy4/maps_react/utils.py
index 07aa9a124..0c9e508b8 100644
--- a/adhocracy4/maps_react/utils.py
+++ b/adhocracy4/maps_react/utils.py
@@ -1,8 +1,4 @@
-import json
-
from django.conf import settings
-from django.urls import reverse
-from django.utils.html import format_html
def get_map_settings(**kwargs):
@@ -29,45 +25,3 @@ def get_map_settings(**kwargs):
# Filter out the keys that have a value of ""
return {key: val for key, val in map_settings.items() if val != ""}
-
-
-def react_tag_factory(tag_name, api_url_name=None):
- """
- :param tag_name: The name of the template tag.
- :param api_url_name: The name of the API URL (optional).
- :return: A formatted HTML string containing the React tag with required props.
-
- This method creates a function that generates a React tag with the given name and
- attributes. It takes the following parameters:
-
- If the `api_url_name` parameter is provided, the `module` parameter must also be
- provided. Otherwise, a `ValueError` is raised.
-
- The function generated by this method takes a variable number of keyword arguments,
- which are used to populate the attributes of the React tag. If the `module`
- parameter is provided and the keyword argument "polygon" is not included, the
- function automatically adds the "polygon" attribute using the `polygon` setting
- from the `module` object.
- """
-
- def func(**kwargs):
- module = kwargs.pop("module", None)
- if module and "polygon" not in kwargs:
- kwargs["polygon"] = module.settings_instance.polygon
-
- attributes = {"map": get_map_settings(**kwargs)}
- if api_url_name:
- if not module:
- raise ValueError("Module must be provided if api_url_name is provided")
- attributes["apiUrl"] = reverse(
- api_url_name, kwargs={"module_pk": module.pk}
- )
-
- return format_html(
- f'',
- attributes=json.dumps(attributes),
- )
-
- # set the correct name on the function
- func.__name__ = tag_name
- return func
diff --git a/jest.config.js b/jest.config.js
index ca88e87bd..aa923a902 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -29,7 +29,13 @@ const config = {
],
transform: {
'^.+\\.[t|j]sx?$': 'babel-jest'
- }
+ },
+ transformIgnorePatterns: [
+ 'node_modules/(?!(@?react-leaflet)/)'
+ ],
+ setupFiles: [
+ '/setupTests.js'
+ ]
}
module.exports = config
diff --git a/setupTests.js b/setupTests.js
new file mode 100644
index 000000000..47fc61ff2
--- /dev/null
+++ b/setupTests.js
@@ -0,0 +1,3 @@
+if (typeof window.URL.createObjectURL === 'undefined') {
+ window.URL.createObjectURL = () => {}
+}