commit
deccfe7873
12 changed files with 191 additions and 69 deletions
|
@ -135,6 +135,11 @@
|
|||
height: 18px;
|
||||
}
|
||||
|
||||
.icon-size-big {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.state-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,10 @@
|
|||
color: $black;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: $default-spacer;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 2 * $default-spacer;
|
||||
}
|
||||
|
|
|
@ -3,20 +3,21 @@ module ApplicationController::ErrorHandling
|
|||
|
||||
included do
|
||||
rescue_from ActionController::InvalidAuthenticityToken do
|
||||
if cookies.count == 0
|
||||
# When some browsers (like Safari) re-open a previously closed tab, they attempts
|
||||
# to reload the page – even if it is a POST request. But in that case, they don’t
|
||||
# sends any of the cookies.
|
||||
#
|
||||
# Ignore this error.
|
||||
render plain: "Les cookies doivent être activés pour utiliser #{APPLICATION_NAME}.", status: 403
|
||||
else
|
||||
# When some browsers (like Safari) re-open a previously closed tab, they attempts
|
||||
# to reload the page – even if it is a POST request. But in that case, they don’t
|
||||
# sends any of the cookies.
|
||||
#
|
||||
# In that case, don’t report this error.
|
||||
if request.cookies.count > 0
|
||||
log_invalid_authenticity_token_error
|
||||
raise # propagate the exception up, to render the default exception page
|
||||
end
|
||||
|
||||
raise # propagate the exception up, to render the default exception page
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_invalid_authenticity_token_error
|
||||
Sentry.with_scope do |temp_scope|
|
||||
tags = {
|
||||
|
|
|
@ -16,16 +16,11 @@ module Users
|
|||
private
|
||||
|
||||
def procedure
|
||||
Procedure.publiees.or(Procedure.brouillons).find_by(path: params[:path])
|
||||
Procedure.publiees_ou_closes.find_by(path: params[:path])
|
||||
end
|
||||
|
||||
def procedure_not_found
|
||||
if procedure&.close?
|
||||
flash.alert = t('errors.messages.procedure_archived')
|
||||
else
|
||||
flash.alert = t('errors.messages.procedure_not_found')
|
||||
end
|
||||
|
||||
flash.alert = t('errors.messages.procedure_not_found')
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,15 @@ import React, { useState } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl';
|
||||
import DrawControl from 'react-mapbox-gl-draw';
|
||||
import { CursorClickIcon } from '@heroicons/react/outline';
|
||||
import {
|
||||
CursorClickIcon,
|
||||
PlusIcon,
|
||||
LocationMarkerIcon
|
||||
} from '@heroicons/react/outline';
|
||||
import CoordinateInput from 'react-coordinate-input';
|
||||
import { fire } from '@utils';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import { useId } from '@reach/auto-id';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
|
||||
|
@ -162,6 +170,87 @@ function MapEditor({ featureCollection, url, options, preview }) {
|
|||
</div>
|
||||
)}
|
||||
</Mapbox>
|
||||
<PointInput />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PointInput() {
|
||||
const inputId = useId();
|
||||
const [value, setValue] = useState('');
|
||||
const [feature, setFeature] = useState(null);
|
||||
const getCurrentPosition = () => {
|
||||
navigator.geolocation &&
|
||||
navigator.geolocation.getCurrentPosition(({ coords }) => {
|
||||
setValue(
|
||||
`${coords.latitude.toPrecision(6)}, ${coords.longitude.toPrecision(
|
||||
6
|
||||
)}`
|
||||
);
|
||||
});
|
||||
};
|
||||
const addPoint = () => {
|
||||
if (feature) {
|
||||
fire(document, 'map:feature:create', feature);
|
||||
setValue('');
|
||||
setFeature(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
className="areas-title mt-1"
|
||||
htmlFor={inputId}
|
||||
style={{ fontSize: '16px' }}
|
||||
>
|
||||
Ajouter un point sur la carte
|
||||
</label>
|
||||
<div className="flex align-center mt-1 mb-2">
|
||||
{navigator.geolocation ? (
|
||||
<button
|
||||
type="button"
|
||||
className="button mr-1"
|
||||
onClick={getCurrentPosition}
|
||||
title="Localiser votre position"
|
||||
>
|
||||
<VisuallyHidden>Localiser votre position</VisuallyHidden>
|
||||
<LocationMarkerIcon className="icon-size-big" />
|
||||
</button>
|
||||
) : null}
|
||||
<CoordinateInput
|
||||
id={inputId}
|
||||
className="m-0 mr-1"
|
||||
value={value}
|
||||
onChange={(value, { dd }) => {
|
||||
setValue(value);
|
||||
setFeature(
|
||||
dd.length
|
||||
? {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: dd.reverse()
|
||||
},
|
||||
properties: {}
|
||||
}
|
||||
: null
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={addPoint}
|
||||
disabled={!feature}
|
||||
title="Ajouter le point avec les coordonnées saisies sur la carte"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
Ajouter le point avec les coordonnées saisies sur la carte
|
||||
</VisuallyHidden>
|
||||
<PlusIcon className="icon-size-big" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Popover, RadioGroup } from '@headlessui/react';
|
|||
import { usePopper } from 'react-popper';
|
||||
import { MapIcon } from '@heroicons/react/outline';
|
||||
import { Slider } from '@reach/slider';
|
||||
import { useId } from '@reach/auto-id';
|
||||
import '@reach/slider/styles.css';
|
||||
|
||||
import { getMapStyle, getLayerName, NBS } from './styles';
|
||||
|
@ -16,12 +17,15 @@ const STYLES = {
|
|||
|
||||
function optionalLayersMap(optionalLayers) {
|
||||
return Object.fromEntries(
|
||||
optionalLayers
|
||||
.filter((layer) => layer != 'cadastres')
|
||||
.map((layer) => [
|
||||
layer,
|
||||
{ enabled: true, opacity: 70, name: getLayerName(layer) }
|
||||
])
|
||||
optionalLayers.map((layer) => [
|
||||
layer,
|
||||
{
|
||||
configurable: layer != 'cadastres',
|
||||
enabled: true,
|
||||
opacity: 70,
|
||||
name: getLayerName(layer)
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -76,6 +80,10 @@ function MapStyleControl({
|
|||
const { styles, attributes } = usePopper(buttonElement, panelElement, {
|
||||
placement: 'bottom-end'
|
||||
});
|
||||
const configurableLayers = Object.entries(layers).filter(
|
||||
([, { configurable }]) => configurable
|
||||
);
|
||||
const mapId = useId();
|
||||
|
||||
return (
|
||||
<div className="form map-style-control mapboxgl-ctrl-group">
|
||||
|
@ -123,42 +131,43 @@ function MapStyleControl({
|
|||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{Object.keys(layers).length ? (
|
||||
{configurableLayers.length ? (
|
||||
<ul className="layers-list">
|
||||
{Object.entries(layers).map(
|
||||
([layer, { enabled, opacity, name }]) => (
|
||||
<li key={layer}>
|
||||
<div className="flex mb-1">
|
||||
<input
|
||||
className="m-0 p-0 mr-1"
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(event) => {
|
||||
setLayerEnabled(layer, event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className="m-0">{name}</label>
|
||||
</div>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={opacity}
|
||||
onChange={(value) => {
|
||||
setLayerOpacity(layer, value);
|
||||
{configurableLayers.map(([layer, { enabled, opacity, name }]) => (
|
||||
<li key={layer}>
|
||||
<div className="flex mb-1">
|
||||
<input
|
||||
id={`${mapId}-${layer}`}
|
||||
className="m-0 p-0 mr-1"
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(event) => {
|
||||
setLayerEnabled(layer, event.target.checked);
|
||||
}}
|
||||
className="mb-1"
|
||||
title={`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`}
|
||||
getAriaLabel={() =>
|
||||
`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`
|
||||
}
|
||||
getAriaValueText={(value) =>
|
||||
`L’opacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<label className="m-0" htmlFor={`${mapId}-${layer}`}>
|
||||
{name}
|
||||
</label>
|
||||
</div>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={opacity}
|
||||
onChange={(value) => {
|
||||
setLayerOpacity(layer, value);
|
||||
}}
|
||||
className="mb-1"
|
||||
title={`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`}
|
||||
getAriaLabel={() =>
|
||||
`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`
|
||||
}
|
||||
getAriaValueText={(value) =>
|
||||
`L’opacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</Popover.Panel>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%li{ class: editing ? '' : 'flex column mb-2' }
|
||||
%li{ class: editing ? 'mb-1' : 'flex column mb-2' }
|
||||
- if editing
|
||||
= link_to '#', data: { geo_area: geo_area.id } do
|
||||
= geo_area_label(geo_area)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- champ.selections_utilisateur.each do |geo_area|
|
||||
= render partial: 'shared/champs/carte/geo_area', locals: { geo_area: geo_area, editing: editing }
|
||||
|
||||
- if champ.cadastres?
|
||||
- if champ.cadastres.present?
|
||||
.areas-title Parcelles cadastrales
|
||||
.areas
|
||||
%ul
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@rails/webpacker": "5.1.1",
|
||||
"@reach/combobox": "^0.13.0",
|
||||
"@reach/slider": "^0.15.0",
|
||||
"@reach/visually-hidden": "^0.15.2",
|
||||
"@sentry/browser": "^5.15.5",
|
||||
"@tmcw/togeojson": "^4.3.0",
|
||||
"babel-plugin-macros": "^2.8.0",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"match-sorter": "^6.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-coordinate-input": "^1.0.0-rc.2",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-mapbox-gl": "^5.1.1",
|
||||
|
|
|
@ -33,15 +33,10 @@ RSpec.describe ApplicationController::ErrorHandling, type: :controller do
|
|||
{}
|
||||
end
|
||||
|
||||
it 'returns a message' do
|
||||
post :invalid_authenticity_token
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
expect(response.body).to include('cookies')
|
||||
end
|
||||
|
||||
it 'renders the standard exception page' do
|
||||
expect { post :invalid_authenticity_token }.not_to raise_error
|
||||
it 'doesn’t log the error' do
|
||||
allow(Sentry).to receive(:capture_message)
|
||||
post :invalid_authenticity_token rescue nil
|
||||
expect(Sentry).not_to have_received(:capture_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -122,7 +122,9 @@ feature 'As an administrateur I can edit types de champ', js: true do
|
|||
preview_window = window_opened_by { click_on 'Prévisualiser le formulaire' }
|
||||
within_window(preview_window) do
|
||||
expect(page).to have_content('Libellé de champ carte')
|
||||
expect(page).to have_content('Parcelles cadastrales')
|
||||
expect(page).to have_content('Ajouter un point sur la carte')
|
||||
fill_in 'Ajouter un point sur la carte', with: "48°52'27\"N 002°54'32\"E"
|
||||
click_on 'Ajouter le point avec les coordonnées saisies sur la carte'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -1740,6 +1740,14 @@
|
|||
tiny-warning "^1.0.3"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@reach/visually-hidden@^0.15.2":
|
||||
version "0.15.2"
|
||||
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.15.2.tgz#07794cb53f4bd23a9452d53a0ad7778711ee323f"
|
||||
integrity sha512-suDSCuKKuqiEB4UDgwWHbrPRxNwrusZ3ImXr85kfsQXGmKptMogaq22xoaHn32NC++lzZXxdWtAJriieszzFXw==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@sentry/browser@^5.15.5":
|
||||
version "5.15.5"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36"
|
||||
|
@ -6597,6 +6605,11 @@ ignore@^5.1.1, ignore@^5.1.4:
|
|||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
||||
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
||||
|
||||
imask@^6.0.5:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/imask/-/imask-6.1.0.tgz#8dcf4c44c51c4297f691349b925b7d5b21edff1c"
|
||||
integrity sha512-IuElVZpc7iuGw+CXfwoUZYpGdoD5H7cGo3S93gZPgjTTWfY5XAXCFL3g3b/ZJBZ18RG2dFF2kxAuuFlXMjq7oQ==
|
||||
|
||||
import-cwd@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
|
@ -10464,6 +10477,13 @@ rc@^1.2.8:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-coordinate-input@^1.0.0-rc.2:
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/react-coordinate-input/-/react-coordinate-input-1.0.0-rc.2.tgz#274aa920ba28318f515f22089ff2f6870a0e66bd"
|
||||
integrity sha512-Umfnh0jEt/bKmIM+B/fT3QscL06CrRo0WRYIMEhgcieoeU5A54hR5WGjS7dKm33sMF5fywGXs7rZ/iyCk/N5yA==
|
||||
dependencies:
|
||||
imask "^6.0.5"
|
||||
|
||||
react-dom@^17.0.1:
|
||||
version "17.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
|
||||
|
@ -12263,7 +12283,7 @@ tslib@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
|
||||
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
|
||||
|
||||
tslib@^2.1.0:
|
||||
tslib@^2.1.0, tslib@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
|
Loading…
Add table
Reference in a new issue