diff --git a/Gemfile.lock b/Gemfile.lock index 98315f70f..eb21c74b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,7 +170,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.8) + concurrent-ruby (1.1.9) connection_pool (2.2.3) crack (0.4.5) rexml @@ -244,7 +244,7 @@ GEM execjs (2.7.0) factory_bot (6.1.0) activesupport (>= 5.0.0) - faraday (1.4.2) + faraday (1.4.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) diff --git a/app/assets/stylesheets/carte.scss b/app/assets/stylesheets/carte.scss index 4e4a6cf70..33f065583 100644 --- a/app/assets/stylesheets/carte.scss +++ b/app/assets/stylesheets/carte.scss @@ -1,4 +1,5 @@ @import "colors"; +@import "constants"; .areas-title { font-weight: bold; @@ -38,6 +39,24 @@ left: 5px; } } + + .map-style-panel { + z-index: 99; + padding: $default-spacer; + margin-bottom: $default-spacer; + + ul { + list-style: none; + padding: $default-spacer; + padding-bottom: 0; + margin-bottom: -$default-spacer; + + label { + font-size: 12px; + font-weight: normal; + } + } + } } .cadastres-selection-control { diff --git a/app/assets/stylesheets/helpers.scss b/app/assets/stylesheets/helpers.scss index 92755dac9..4577ef84c 100644 --- a/app/assets/stylesheets/helpers.scss +++ b/app/assets/stylesheets/helpers.scss @@ -1,11 +1,15 @@ @import "constants"; +.m-0 { + margin: 0px !important; +} + .mb-1 { margin-bottom: $default-spacer; } .mr-1 { - margin-right: $default-spacer; + margin-right: $default-spacer !important; } .mb-4 { @@ -20,6 +24,10 @@ padding-left: 0px !important; } +.p-0 { + padding: 0px !important; +} + .bold { font-weight: bold; } diff --git a/app/controllers/experts/avis_controller.rb b/app/controllers/experts/avis_controller.rb index 04114174a..1bc066d91 100644 --- a/app/controllers/experts/avis_controller.rb +++ b/app/controllers/experts/avis_controller.rb @@ -4,7 +4,7 @@ module Experts before_action :authenticate_expert!, except: [:sign_up, :update_expert] before_action :check_if_avis_revoked, only: [:show] - before_action :redirect_if_no_sign_up_needed, only: [:sign_up] + before_action :redirect_if_no_sign_up_needed, only: [:sign_up, :update_expert] before_action :set_avis_and_dossier, only: [:show, :instruction, :messagerie, :create_commentaire, :update] A_DONNER_STATUS = 'a-donner' @@ -82,12 +82,11 @@ module Experts email = params[:email] password = params[:user][:password] - # Not perfect because the password will not be changed if the user already exists user = User.create_or_promote_to_expert(email, password) + user.reset_password(password, password) if user.valid? sign_in(user) - redirect_to url_for(expert_all_avis_path) else flash[:alert] = user.errors.full_messages @@ -128,11 +127,9 @@ module Experts if current_expert.present? # an expert is authenticated ... lets see if it can view the dossier - redirect_to expert_avis_url(avis.procedure, avis) - elsif avis.expert&.email == params[:email] && avis.expert.user.active?.present? - + # The expert already used the sign-in page to change their password: ask them to sign-in instead. redirect_to new_user_session_url end end diff --git a/app/controllers/instructeurs/archives_controller.rb b/app/controllers/instructeurs/archives_controller.rb index cadf9b5e9..24e4bdfe2 100644 --- a/app/controllers/instructeurs/archives_controller.rb +++ b/app/controllers/instructeurs/archives_controller.rb @@ -7,7 +7,6 @@ module Instructeurs @average_dossier_weight = procedure.average_dossier_weight @count_dossiers_termines_by_month = Traitement.count_dossiers_termines_by_month(groupe_instructeurs) - @nb_dossiers_termines = @count_dossiers_termines_by_month.sum { |count_by_month| count_by_month["count"] } @archives = Archive .for_groupe_instructeur(groupe_instructeurs) diff --git a/app/controllers/new_administrateur/administrateur_controller.rb b/app/controllers/new_administrateur/administrateur_controller.rb index 7c5f4cdda..f4c4a1b0f 100644 --- a/app/controllers/new_administrateur/administrateur_controller.rb +++ b/app/controllers/new_administrateur/administrateur_controller.rb @@ -27,7 +27,7 @@ module NewAdministrateur end def reset_procedure - if @procedure.brouillon? + if @procedure.brouillon? || @procedure.draft_changed? @procedure.reset! end end diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb index d5a99c579..1e5a07877 100644 --- a/app/controllers/new_administrateur/procedures_controller.rb +++ b/app/controllers/new_administrateur/procedures_controller.rb @@ -55,11 +55,8 @@ module NewAdministrateur def show @procedure = current_administrateur.procedures.find(params[:id]) @current_administrateur = current_administrateur - if @procedure.brouillon? - @procedure_lien = commencer_test_url(path: @procedure.path) - else - @procedure_lien = commencer_url(path: @procedure.path) - end + @procedure_lien = commencer_url(path: @procedure.path) + @procedure_lien_test = commencer_test_url(path: @procedure.path) end def edit @@ -142,11 +139,8 @@ module NewAdministrateur end def publication - if @procedure.brouillon? - @procedure_lien = commencer_test_url(path: @procedure.path) - else - @procedure_lien = commencer_url(path: @procedure.path) - end + @procedure_lien = commencer_url(path: @procedure.path) + @procedure_lien_test = commencer_test_url(path: @procedure.path) @procedure.path = @procedure.suggested_path(current_administrateur) @current_administrateur = current_administrateur end diff --git a/app/controllers/users/commencer_controller.rb b/app/controllers/users/commencer_controller.rb index cd22a0664..5d29d07c4 100644 --- a/app/controllers/users/commencer_controller.rb +++ b/app/controllers/users/commencer_controller.rb @@ -5,13 +5,15 @@ module Users def commencer @procedure = retrieve_procedure return procedure_not_found if @procedure.blank? || @procedure.brouillon? + @revision = @procedure.published_revision render 'commencer/show' end def commencer_test @procedure = retrieve_procedure - return procedure_not_found if @procedure.blank? || @procedure.publiee? + return procedure_not_found if @procedure.blank? || (@procedure.publiee? && !@procedure.draft_changed?) + @revision = @procedure.draft_revision render 'commencer/show' end @@ -20,14 +22,14 @@ module Users @procedure = retrieve_procedure return procedure_not_found if @procedure.blank? || @procedure.brouillon? - generate_empty_pdf(@procedure) + generate_empty_pdf(@procedure.published_revision) end def dossier_vide_pdf_test @procedure = retrieve_procedure - return procedure_not_found if @procedure.blank? || @procedure.publiee? + return procedure_not_found if @procedure.blank? || (@procedure.publiee? && !@procedure.draft_changed?) - generate_empty_pdf(@procedure) + generate_empty_pdf(@procedure.draft_revision) end def sign_in @@ -80,10 +82,10 @@ module Users store_location_for(:user, helpers.procedure_lien(procedure)) end - def generate_empty_pdf(procedure) - @dossier = procedure.active_revision.new_dossier + def generate_empty_pdf(revision) + @dossier = revision.new_dossier s = render_to_string(template: 'dossiers/dossier_vide', formats: [:pdf]) - send_data(s, :filename => "#{procedure.libelle}.pdf") + send_data(s, :filename => "#{revision.procedure.libelle}.pdf") end end end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index daf8d0387..bafdd8777 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -242,18 +242,18 @@ module Users erase_user_location! begin - if params[:brouillon] - procedure = Procedure.brouillon.find(params[:procedure_id]) + procedure = if params[:brouillon] + Procedure.publiees.or(Procedure.brouillons).find(params[:procedure_id]) else - procedure = Procedure.publiees.find(params[:procedure_id]) + Procedure.publiees.find(params[:procedure_id]) end rescue ActiveRecord::RecordNotFound flash.alert = t('errors.messages.procedure_not_found') - return redirect_to url_for dossiers_path + return redirect_to dossiers_path end dossier = Dossier.new( - revision: procedure.active_revision, + revision: params[:brouillon] ? procedure.draft_revision : procedure.active_revision, groupe_instructeur: procedure.defaut_groupe_instructeur_for_new_dossier, user: current_user, state: Dossier.states.fetch(:brouillon) @@ -303,7 +303,7 @@ module Users end def show_demarche_en_test_banner - if @dossier.present? && @dossier.procedure.brouillon? + if @dossier.present? && @dossier.revision.draft? flash.now.alert = "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l'administrateur (ajout d’un champ, publication de la démarche...) entraînera sa suppression." end end diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index 06827d1f9..1891b8485 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -25,12 +25,8 @@ module DossierHelper end end - def url_for_new_dossier(procedure) - if procedure.brouillon? - new_dossier_url(procedure_id: procedure.id, brouillon: true) - else - new_dossier_url(procedure_id: procedure.id) - end + def url_for_new_dossier(revision) + new_dossier_url(procedure_id: revision.procedure.id, brouillon: revision.draft? ? true : nil) end def dossier_form_class(dossier) diff --git a/app/javascript/components/MapEditor/index.jsx b/app/javascript/components/MapEditor/index.jsx index fcedfba4f..1e476bbb7 100644 --- a/app/javascript/components/MapEditor/index.jsx +++ b/app/javascript/components/MapEditor/index.jsx @@ -2,7 +2,7 @@ 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 { MapIcon } from '@heroicons/react/outline'; +import { CursorClickIcon } from '@heroicons/react/outline'; import 'mapbox-gl/dist/mapbox-gl.css'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; @@ -36,7 +36,13 @@ function MapEditor({ featureCollection, url, options, preview }) { enabled: !preview, cadastreEnabled }); - const [style, setStyle] = useMapStyle(options.layers, { + const { + style, + layers, + setStyle, + setLayerEnabled, + setLayerOpacity + } = useMapStyle(options.layers, { onStyleChange, cadastreEnabled }); @@ -133,7 +139,13 @@ function MapEditor({ featureCollection, url, options, preview }) { }} /> )} - + {options.layers.includes('cadastres') && (
@@ -145,7 +157,7 @@ function MapEditor({ featureCollection, url, options, preview }) { title="Sélectionner les parcelles cadastrales" className={cadastreEnabled ? 'on' : ''} > - +
)} diff --git a/app/javascript/components/MapReader/index.jsx b/app/javascript/components/MapReader/index.jsx index c976464f8..2d245b046 100644 --- a/app/javascript/components/MapReader/index.jsx +++ b/app/javascript/components/MapReader/index.jsx @@ -20,7 +20,13 @@ const MapReader = ({ featureCollection, options }) => { onMouseEnter, onMouseLeave } = useMapbox(featureCollection); - const [style, setStyle] = useMapStyle(options.layers, { onStyleChange }); + const { + style, + layers, + setStyle, + setLayerEnabled, + setLayerOpacity + } = useMapStyle(options.layers, { onStyleChange }); if (!isSupported) { return ( @@ -36,7 +42,7 @@ const MapReader = ({ featureCollection, options }) => { onLoad(map)} style={style} - containerStyle={{ height: '400px' }} + containerStyle={{ height: '500px' }} > { onMouseLeave={onMouseLeave} /> - + ); diff --git a/app/javascript/components/shared/mapbox/MapStyleControl.jsx b/app/javascript/components/shared/mapbox/MapStyleControl.jsx index 5a102031b..872389c81 100644 --- a/app/javascript/components/shared/mapbox/MapStyleControl.jsx +++ b/app/javascript/components/shared/mapbox/MapStyleControl.jsx @@ -1,35 +1,28 @@ import React, { useMemo, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { Popover, RadioGroup } from '@headlessui/react'; +import { usePopper } from 'react-popper'; +import { MapIcon } from '@heroicons/react/outline'; +import { Slider } from '@reach/slider'; +import '@reach/slider/styles.css'; -import { getMapStyle } from './styles'; -import ortho from './styles/images/preview-ortho.png'; -import vector from './styles/images/preview-vector.png'; +import { getMapStyle, getLayerName, NBS } from './styles'; const STYLES = { - ortho: { - title: 'Satellite', - preview: ortho, - color: '#fff' - }, - vector: { - title: 'Vectoriel', - preview: vector, - color: '#000' - }, - ign: { - title: 'Carte IGN', - preview: vector, - color: '#000' - } + ortho: 'Satellite', + vector: 'Vectoriel', + ign: 'Carte IGN' }; -function getNextStyle(style) { - const styleNames = Object.keys(STYLES); - const index = styleNames.indexOf(style) + 1; - if (index === styleNames.length) { - return styleNames[0]; - } - return styleNames[index]; +function optionalLayersMap(optionalLayers) { + return Object.fromEntries( + optionalLayers + .filter((layer) => layer != 'cadastres') + .map((layer) => [ + layer, + { enabled: true, opacity: 70, name: getLayerName(layer) } + ]) + ); } export function useMapStyle( @@ -37,33 +30,149 @@ export function useMapStyle( { onStyleChange, cadastreEnabled } ) { const [styleId, setStyle] = useState('ortho'); - const style = useMemo(() => getMapStyle(styleId, optionalLayers), [ - styleId, - optionalLayers - ]); + const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers)); + const setLayerEnabled = (layer, enabled) => + setLayers((optionalLayers) => { + optionalLayers[layer].enabled = enabled; + return { ...optionalLayers }; + }); + const setLayerOpacity = (layer, opacity) => + setLayers((optionalLayers) => { + optionalLayers[layer].opacity = opacity; + return { ...optionalLayers }; + }); + const enabledLayers = Object.entries(layers).filter( + ([, { enabled }]) => enabled + ); + const style = useMemo( + () => + getMapStyle( + styleId, + enabledLayers.map(([layer]) => layer), + Object.fromEntries( + enabledLayers.map(([layer, { opacity }]) => [layer, opacity]) + ) + ), + [ + styleId, + enabledLayers.map(([layer, { opacity }]) => `${layer}-${opacity}`) + ] + ); useEffect(() => onStyleChange(), [styleId, cadastreEnabled]); - return [style, setStyle]; + return { style, layers, setStyle, setLayerEnabled, setLayerOpacity }; } -function MapStyleControl({ style, setStyle }) { - const nextStyle = getNextStyle(style); - const { title, preview, color } = STYLES[nextStyle]; +function MapStyleControl({ + style, + layers, + setStyle, + setLayerEnabled, + setLayerOpacity +}) { + const [buttonElement, setButtonElement] = useState(); + const [panelElement, setPanelElement] = useState(); + const { styles, attributes } = usePopper(buttonElement, panelElement, { + placement: 'bottom-end' + }); return ( -
- +
+ + + + + + + {Object.entries(STYLES).map(([style, title]) => ( + + {({ checked }) => ( + <> + + + {title.replace(/\s/g, ' ')} + + + )} + + ))} + + {Object.keys(layers).length ? ( +
    + {Object.entries(layers).map( + ([layer, { enabled, opacity, name }]) => ( +
  • +
    + { + setLayerEnabled(layer, event.target.checked); + }} + /> + +
    + { + 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}%` + } + /> +
  • + ) + )} +
+ ) : null} +
+
); } MapStyleControl.propTypes = { style: PropTypes.string, - setStyle: PropTypes.func + layers: PropTypes.object, + setStyle: PropTypes.func, + setLayerEnabled: PropTypes.func, + setLayerOpacity: PropTypes.func }; export default MapStyleControl; diff --git a/app/javascript/components/shared/mapbox/styles/base.js b/app/javascript/components/shared/mapbox/styles/base.js index e233e607f..172187d57 100644 --- a/app/javascript/components/shared/mapbox/styles/base.js +++ b/app/javascript/components/shared/mapbox/styles/base.js @@ -119,9 +119,9 @@ const OPTIONAL_LAYERS = [ function buildSources() { return Object.fromEntries( OPTIONAL_LAYERS.filter(({ id }) => id !== 'cadastres') - .flatMap(({ layers }) => layers) - .map(([, code]) => [ - code.toLowerCase().replace(/\./g, '-'), + .flatMap(({ layers }) => layers.map(([, code]) => code)) + .map((code) => [ + getLayerCode(code), rasterSource([ignServiceURL(code)], 'IGN-F/Géoportail/MNHN') ]) ); @@ -138,25 +138,40 @@ function rasterSource(tiles, attribution) { }; } -function rasterLayer(source) { +function rasterLayer(source, opacity) { return { id: source, source, type: 'raster', - paint: { 'raster-resampling': 'linear' } + paint: { 'raster-resampling': 'linear', 'raster-opacity': opacity } }; } -export function buildOptionalLayers(ids) { +export function buildOptionalLayers(ids, opacity) { return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id)) - .flatMap(({ layers }) => layers) - .flatMap(([, code]) => + .flatMap(({ layers, id }) => + layers.map(([, code]) => [code, opacity[id] / 100]) + ) + .flatMap(([code, opacity]) => code === 'CADASTRE' ? cadastreLayers - : [rasterLayer(code.toLowerCase().replace(/\./g, '-'))] + : [rasterLayer(getLayerCode(code), opacity)] ); } +export const NBS = ' '; + +export function getLayerName(layer) { + return OPTIONAL_LAYERS.find(({ id }) => id == layer).label.replace( + /\s/g, + NBS + ); +} + +function getLayerCode(code) { + return code.toLowerCase().replace(/\./g, '-'); +} + export default { version: 8, metadat: { diff --git a/app/javascript/components/shared/mapbox/styles/images/preview-ortho.png b/app/javascript/components/shared/mapbox/styles/images/preview-ortho.png deleted file mode 100644 index 46db7e467..000000000 Binary files a/app/javascript/components/shared/mapbox/styles/images/preview-ortho.png and /dev/null differ diff --git a/app/javascript/components/shared/mapbox/styles/images/preview-vector.png b/app/javascript/components/shared/mapbox/styles/images/preview-vector.png deleted file mode 100644 index 730fc77d2..000000000 Binary files a/app/javascript/components/shared/mapbox/styles/images/preview-vector.png and /dev/null differ diff --git a/app/javascript/components/shared/mapbox/styles/index.js b/app/javascript/components/shared/mapbox/styles/index.js index 218cd5ddf..ed97b2aee 100644 --- a/app/javascript/components/shared/mapbox/styles/index.js +++ b/app/javascript/components/shared/mapbox/styles/index.js @@ -1,9 +1,11 @@ -import baseStyle, { buildOptionalLayers } from './base'; +import baseStyle, { buildOptionalLayers, getLayerName, NBS } from './base'; import orthoStyle from './layers/ortho'; import vectorStyle from './layers/vector'; import ignLayers from './layers/ign'; -export function getMapStyle(id, optionalLayers) { +export { getLayerName, NBS }; + +export function getMapStyle(id, layers, opacity) { const style = { ...baseStyle, id }; switch (id) { @@ -21,7 +23,7 @@ export function getMapStyle(id, optionalLayers) { break; } - style.layers = style.layers.concat(buildOptionalLayers(optionalLayers)); + style.layers = style.layers.concat(buildOptionalLayers(layers, opacity)); return style; } diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 589f2d741..e02100f4b 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -307,9 +307,7 @@ class Procedure < ApplicationRecord end def reset! - if locked? - raise "Can not reset a locked procedure." - else + if !locked? || draft_changed? draft_revision.dossiers.destroy_all end end @@ -426,7 +424,9 @@ class Procedure < ApplicationRecord } } include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin - procedure = self.deep_clone(include: include_list, &method(:clone_attachments)) + procedure = self.deep_clone(include: include_list) do |original, kopy| + PiecesJustificativesService.clone_attachments(original, kopy) + end procedure.path = SecureRandom.uuid procedure.aasm_state = :brouillon procedure.closed_at = nil @@ -472,29 +472,6 @@ class Procedure < ApplicationRecord procedure end - def clone_attachments(original, kopy) - if original.is_a?(TypeDeChamp) - clone_attachment(:piece_justificative_template, original, kopy) - elsif original.is_a?(Procedure) - clone_attachment(:logo, original, kopy) - clone_attachment(:notice, original, kopy) - clone_attachment(:deliberation, original, kopy) - end - end - - def clone_attachment(attribute, original, kopy) - original_attachment = original.send(attribute) - if original_attachment.attached? - kopy.send(attribute).attach({ - io: StringIO.new(original_attachment.download), - filename: original_attachment.filename, - content_type: original_attachment.content_type, - # we don't want to run virus scanner on cloned file - metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } - }) - end - end - def whitelisted? whitelisted_at.present? end diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index 13c60c8d4..75513d724 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -249,7 +249,9 @@ class ProcedureRevision < ApplicationRecord def revise_type_de_champ(type_de_champ) types_de_champ_association = type_de_champ.private? ? :revision_types_de_champ_private : :revision_types_de_champ association = send(types_de_champ_association).find_by!(type_de_champ: type_de_champ) - cloned_type_de_champ = type_de_champ.deep_clone(include: [:types_de_champ], &type_de_champ.method(:clone_attachments)) + cloned_type_de_champ = type_de_champ.deep_clone(include: [:types_de_champ]) do |original, kopy| + PiecesJustificativesService.clone_attachments(original, kopy) + end cloned_type_de_champ.revision = self association.update!(type_de_champ: cloned_type_de_champ) cloned_type_de_champ diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 8874766fc..999d47f23 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -369,23 +369,4 @@ class TypeDeChamp < ApplicationRecord types_de_champ.destroy_all end end - - def clone_attachments(original, kopy) - if original.is_a?(TypeDeChamp) - clone_attachment(:piece_justificative_template, original, kopy) - end - end - - def clone_attachment(attribute, original, kopy) - original_attachment = original.send(attribute) - if original_attachment.attached? - kopy.send(attribute).attach({ - io: StringIO.new(original_attachment.download), - filename: original_attachment.filename, - content_type: original_attachment.content_type, - # we don't want to run virus scanner on cloned file - metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } - }) - end - end end diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index c70ace4dc..974e774b8 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -49,6 +49,30 @@ class PiecesJustificativesService end end + def self.clone_attachments(original, kopy) + if original.is_a?(TypeDeChamp) + clone_attachment(original.piece_justificative_template, kopy.piece_justificative_template) + elsif original.is_a?(Procedure) + clone_attachment(original.logo, kopy.logo) + clone_attachment(original.notice, kopy.notice) + clone_attachment(original.deliberation, kopy.deliberation) + end + end + + def self.clone_attachment(original_attachment, copy_attachment) + if original_attachment.attached? + original_attachment.open do |tempfile| + copy_attachment.attach({ + io: File.open(tempfile.path), + filename: original_attachment.filename, + content_type: original_attachment.content_type, + # we don't want to run virus scanner on cloned file + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + }) + end + end + end + class FakeAttachment < Hashie::Dash property :filename property :name diff --git a/app/views/commencer/show.html.haml b/app/views/commencer/show.html.haml index 7abafdd4a..ccd867332 100644 --- a/app/views/commencer/show.html.haml +++ b/app/views/commencer/show.html.haml @@ -11,12 +11,12 @@ = link_to 'J’ai déjà un compte', commencer_sign_in_path(path: @procedure.path), class: ['button large expand'] - else - - dossiers = current_user.dossiers.where(revision: @procedure.revisions) + - dossiers = current_user.dossiers.where(revision: @revision.draft? ? @revision : @procedure.revisions.where.not(id: @procedure.draft_revision_id)) - drafts = dossiers.merge(Dossier.state_brouillon) - not_drafts = dossiers.merge(Dossier.state_not_brouillon) - if dossiers.count == 0 - = link_to 'Commencer la démarche', url_for_new_dossier(@procedure), class: ['button large expand primary'] + = link_to 'Commencer la démarche', url_for_new_dossier(@revision), class: ['button large expand primary'] - elsif drafts.count == 1 && not_drafts.count == 0 - dossier = drafts.first @@ -25,7 +25,7 @@ Il y a #{time_ago_in_words(dossier.created_at)}, vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ». = link_to 'Continuer à remplir mon dossier', brouillon_dossier_path(dossier), class: ['button large expand primary'] - = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] + = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand'] - elsif not_drafts.count == 1 - dossier = not_drafts.first @@ -34,17 +34,15 @@ Il y a #{time_ago_in_words(dossier.en_construction_at)}, vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ». = link_to 'Voir mon dossier déposé', dossier_path(dossier), class: ['button large expand primary'] - = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] + = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand'] - else %h2.huge-title Vous avez déjà des dossiers pour cette démarche = link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary'] - = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand'] + = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@revision), class: ['button large expand'] - if @procedure.feature_enabled?(:dossier_pdf_vide) - - pdf_link = commencer_dossier_vide_path(path: @procedure.path) - - if @procedure.brouillon? - - pdf_link = commencer_dossier_vide_test_path(path: @procedure.path) + - pdf_link = @revision.draft? ? commencer_dossier_vide_test_path(path: @procedure.path) : commencer_dossier_vide_path(path: @procedure.path) %hr %p Vous souhaitez effectuer une demande par papier ? Vous pouvez télécharger un dossier vide au format PDF, diff --git a/app/views/instructeurs/archives/index.html.haml b/app/views/instructeurs/archives/index.html.haml index 03d6e53b4..941d3d460 100644 --- a/app/views/instructeurs/archives/index.html.haml +++ b/app/views/instructeurs/archives/index.html.haml @@ -30,31 +30,6 @@ %th.center Télécharger %tbody - %tr - - matching_archive = @archives.find { |archive| archive.time_span_type == 'everything' } - - weight = estimate_weight(matching_archive, @nb_dossiers_termines, @average_dossier_weight) - %td - Tous les dossiers - %td.text-right - = @nb_dossiers_termines - %td.text-right - = number_to_human_size(weight) - %td.center - - if matching_archive.try(&:available?) - = link_to url_for(matching_archive.file), class: 'button primary' do - %span.icon.download-white - = t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure]) - - elsif matching_archive.try(&:pending?) - %span.icon.retry - = t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure]) - - elsif @nb_dossiers_termines == 0 - Rien à télécharger ! - - elsif weight < 1.gigabyte - = link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button" do - %span.icon.new-folder - Demander la création - - else - Archive trop volumineuse - @count_dossiers_termines_by_month.each do |count_by_month| - month = count_by_month["month"].to_date - nb_dossiers_termines = count_by_month["count"] diff --git a/app/views/layouts/_header.haml b/app/views/layouts/_header.haml index dbf2ba1a7..80e64b4d5 100644 --- a/app/views/layouts/_header.haml +++ b/app/views/layouts/_header.haml @@ -25,11 +25,7 @@ %li = active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link' - if current_instructeur.user.expert && current_expert.avis_summary[:total] > 0 - %li - = active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do - Avis - - if current_expert.avis_summary[:unanswered] > 0 - %span.badge.warning= current_expert.avis_summary[:unanswered] + = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } - if nav_bar_profile == :expert && expert_signed_in? %ul.header-tabs @@ -38,16 +34,14 @@ = active_link_to "Démarches", instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'tab-link' - if current_expert.avis_summary[:total] > 0 - %li - = active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do - Avis - - if current_expert.avis_summary[:unanswered] > 0 - %span.badge.warning= current_expert.avis_summary[:unanswered] + = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } - if nav_bar_profile == :user %ul.header-tabs %li = active_link_to "Dossiers", dossiers_path, active: :inclusive, class: 'tab-link' + - if current_user.expert && current_expert.avis_summary[:total] > 0 + = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } %ul.header-right-content - if params[:controller] == 'recherche' diff --git a/app/views/layouts/header/_avis_tab.html.haml b/app/views/layouts/header/_avis_tab.html.haml new file mode 100644 index 000000000..79eab2490 --- /dev/null +++ b/app/views/layouts/header/_avis_tab.html.haml @@ -0,0 +1,5 @@ +%li + = active_link_to expert_all_avis_path, active: controller_name == 'avis', class: 'tab-link' do + Avis + - if current_expert.avis_summary[:unanswered] > 0 + %span.badge.warning= current_expert.avis_summary[:unanswered] diff --git a/app/views/new_administrateur/procedures/publication.html.haml b/app/views/new_administrateur/procedures/publication.html.haml index 4b07fd278..281966184 100644 --- a/app/views/new_administrateur/procedures/publication.html.haml +++ b/app/views/new_administrateur/procedures/publication.html.haml @@ -28,7 +28,7 @@ %p Cette démarche est actuellement en test, pour y accéder vous pouvez utiliser le lien : - = link_to @procedure_lien, sanitize_url(@procedure_lien), target: :blank, rel: :noopener + = link_to @procedure_lien_test, sanitize_url(@procedure_lien_test), target: :blank, rel: :noopener %p.mb-4 Toute personne ayant la connaissance de ce lien pourra ainsi remplir des dossiers de test sur votre démarche. diff --git a/app/views/new_administrateur/procedures/show.html.haml b/app/views/new_administrateur/procedures/show.html.haml index 9182af12f..f747f03f6 100644 --- a/app/views/new_administrateur/procedures/show.html.haml +++ b/app/views/new_administrateur/procedures/show.html.haml @@ -8,8 +8,8 @@ %span.icon.preview Prévisualiser - - if @procedure.brouillon? - = link_to sanitize_url(@procedure_lien), target: :blank, rel: :noopener, class: 'button' do + - if @procedure.brouillon? || @procedure.draft_changed? + = link_to sanitize_url(@procedure_lien_test), target: :blank, rel: :noopener, class: 'button' do %span.icon.in-progress Tester diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 75bc84ece..442a5f69f 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -7,13 +7,5 @@ Sentry.init do |config| config.enabled_environments = ['production', secrets[:environment].presence].compact config.breadcrumbs_logger = [:active_support_logger] config.traces_sample_rate = secrets[:enabled] ? 0.001 : nil - config.excluded_exceptions += [ - # Ignore exceptions caught by ActiveJob.retry_on - # https://github.com/getsentry/sentry-ruby/issues/1347 - 'Excon::Error::BadRequest', - 'ActiveStorage::IntegrityError', - 'VirusScannerJob::FileNotAnalyzedYetError', - 'TitreIdentiteWatermarkJob::WatermarkFileNotScannedYetError', - 'APIEntreprise::API::Error::TimedOut' - ] + config.delayed_job.report_after_job_retries = true end diff --git a/package.json b/package.json index 263362691..a8d0871bd 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "dependencies": { "@babel/preset-react": "^7.12.13", + "@headlessui/react": "^1.3.0", "@heroicons/react": "^1.0.1", "@mapbox/mapbox-gl-draw": "^1.2.2", + "@popperjs/core": "^2.9.2", "@rails/actiontext": "^6.0.3", "@rails/activestorage": "^6.0.3", "@rails/ujs": "^6.0.3", "@rails/webpacker": "5.1.1", "@reach/combobox": "^0.13.0", + "@reach/slider": "^0.15.0", "@sentry/browser": "^5.15.5", "@tmcw/togeojson": "^4.3.0", "babel-plugin-macros": "^2.8.0", @@ -28,6 +31,7 @@ "react-intersection-observer": "^8.31.0", "react-mapbox-gl": "^5.1.1", "react-mapbox-gl-draw": "^2.0.4", + "react-popper": "^2.2.5", "react-query": "^3.9.7", "react-sortable-hoc": "^1.11.0", "react_ujs": "^2.6.1", diff --git a/public/422.html b/public/422.html index a21f82b3b..899adfc29 100644 --- a/public/422.html +++ b/public/422.html @@ -1,67 +1,59 @@ - + - The change you wanted was rejected (422) - + + + + + + Erreur 422 · demarches-simplifiees.fr + - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

+
+
+
+
+ +
+
+
+
+

L’action demandée a été rejetée.

+

+ Pas de panique, c’est probablement temporaire. +

+

+ Essayez de + recharger la page précédente, + et tout devrait rentrer dans l’ordre. +

-

If you are the application owner check the logs for more information.

diff --git a/spec/controllers/experts/avis_controller_spec.rb b/spec/controllers/experts/avis_controller_spec.rb index 0439df956..c81d0f472 100644 --- a/spec/controllers/experts/avis_controller_spec.rb +++ b/spec/controllers/experts/avis_controller_spec.rb @@ -300,54 +300,83 @@ describe Experts::AvisController, type: :controller do end context 'without an expert signed in' do + let(:claimant) { create(:instructeur) } + let(:expert) { create(:expert) } + let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: procedure) } + let(:dossier) { create(:dossier) } + let(:avis) { create(:avis, dossier: dossier, experts_procedure: experts_procedure, claimant: claimant) } + let(:procedure) { dossier.procedure } + describe '#sign_up' do - let(:invited_email) { 'invited@avis.com' } - let(:claimant) { create(:instructeur) } - let(:expert) { create(:expert) } - let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: procedure) } - let(:dossier) { create(:dossier) } - let(:procedure) { dossier.procedure } - let!(:avis) { create(:avis, experts_procedure: experts_procedure, claimant: claimant, dossier: dossier) } - let(:invitations_email) { true } - - context 'when the expert has already signed up and belongs to the invitation' do - let!(:avis) { create(:avis, dossier: dossier, experts_procedure: experts_procedure, claimant: claimant) } - - context 'when the expert is authenticated' do - before do - sign_in(expert.user) - expert.user.update(last_sign_in_at: Time.zone.now) - expert.user.reload - get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email } - end - - it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) } - end - - context 'when the expert is not authenticated' do - before do - sign_in(expert.user) - expert.user.update(last_sign_in_at: Time.zone.now) - expert.user.reload - sign_out(expert.user) - get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email } - end - - it { is_expected.to redirect_to new_user_session_url } - end + subject do + get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email } end - context 'when the expert has already signed up / is authenticated and does not belong to the invitation' do - let(:expert) { create(:expert) } - let!(:avis) { create(:avis, email: invited_email, dossier: dossier, experts_procedure: experts_procedure) } + context 'when the expert hasn’t signed up yet' do + before { expert.user.update(last_sign_in_at: nil) } - before do - sign_in(expert.user) - get :sign_up, params: { id: avis.id, procedure_id: procedure.id, email: avis.expert.email } + it { is_expected.to have_http_status(:success) } + end + + context 'when the expert has already signed up' do + before { expert.user.update(last_sign_in_at: Time.zone.now) } + + context 'and the expert belongs to the invitation' do + context 'and the expert is authenticated' do + before { sign_in(expert.user) } + + it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) } + end + + context 'and the expert is not authenticated' do + before { sign_out(expert.user) } + + it { is_expected.to redirect_to new_user_session_url } + end end - # redirected to dossier but then the instructeur gonna be banished ! - it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) } + context 'and the expert does not belong to the invitation' do + let(:avis) { create(:avis, email: 'another_expert@avis.com', dossier: dossier, experts_procedure: experts_procedure) } + + before { sign_in(expert.user) } + # redirected to dossier but then the instructeur gonna be banished ! + it { is_expected.to redirect_to expert_avis_url(avis.procedure, avis) } + end + end + end + + describe '#update_expert' do + subject do + post :update_expert, params: { + id: avis.id, + procedure_id: procedure.id, + email: avis.expert.email, + user: { + password: 'my-s3cure-p4ssword' + } + } + end + + context 'when the expert hasn’t signed up yet' do + before { expert.user.update(last_sign_in_at: nil) } + + it 'saves the expert new password' do + subject + expect(expert.user.reload.valid_password?('my-s3cure-p4ssword')).to be true + end + + it { is_expected.to redirect_to expert_all_avis_path } + end + + context 'when the expert has already signed up' do + before { expert.user.update(last_sign_in_at: Time.zone.now) } + + it 'doesn’t change the expert password' do + subject + expect(expert.user.reload.valid_password?('my-s3cure-p4ssword')).to be false + end + + it { is_expected.to redirect_to new_user_session_url } end end end diff --git a/spec/controllers/instructeurs/archives_controller_spec.rb b/spec/controllers/instructeurs/archives_controller_spec.rb index e039abce9..c5cb89dae 100644 --- a/spec/controllers/instructeurs/archives_controller_spec.rb +++ b/spec/controllers/instructeurs/archives_controller_spec.rb @@ -25,7 +25,6 @@ describe Instructeurs::ArchivesController, type: :controller do it 'displays archives' do get :index, { params: { procedure_id: procedure1.id } } - expect(assigns(:nb_dossiers_termines).size).to eq(8) expect(assigns(:archives)).to eq([archive1]) end end diff --git a/spec/controllers/users/commencer_controller_spec.rb b/spec/controllers/users/commencer_controller_spec.rb index bf80703e8..39730b912 100644 --- a/spec/controllers/users/commencer_controller_spec.rb +++ b/spec/controllers/users/commencer_controller_spec.rb @@ -13,6 +13,7 @@ describe Users::CommencerController, type: :controller do expect(subject.status).to eq(200) expect(subject).to render_template('show') expect(assigns(:procedure)).to eq published_procedure + expect(assigns(:revision)).to eq published_procedure.published_revision end end @@ -43,6 +44,7 @@ describe Users::CommencerController, type: :controller do expect(subject.status).to eq(200) expect(subject).to render_template('show') expect(assigns(:procedure)).to eq draft_procedure + expect(assigns(:revision)).to eq draft_procedure.draft_revision end end diff --git a/spec/features/experts/expert_spec.rb b/spec/features/experts/expert_spec.rb index c2aea0a66..5a1398da7 100644 --- a/spec/features/experts/expert_spec.rb +++ b/spec/features/experts/expert_spec.rb @@ -11,16 +11,30 @@ feature 'Inviting an expert:' do let(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure, confidentiel: true) } context 'when I don’t already have an account' do - scenario 'I can sign up' do + let(:password) { 'This is an expert password' } + + before 'Signing up' do visit sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email) expect(page).to have_field('Email', with: avis.expert.email, disabled: true) - fill_in 'Mot de passe', with: 'This is a very complicated password !' + fill_in 'Mot de passe', with: password click_on 'Créer un compte' + end + scenario 'I can see the avis after signing up' do expect(page).to have_current_path(expert_all_avis_path) expect(page).to have_text('1 avis à donner') end + + scenario 'I can sign-in again afterwards' do + click_on 'Se déconnecter' + + visit new_user_session_path + sign_in_with avis.expert.email, password + + expect(page).to have_content('Connecté(e).') + expect(page).to have_current_path(dossiers_path) # Ideally we'd want `expert_all_avis_path` instead + end end context 'when I already have an existing account' do diff --git a/spec/views/commencer/show.html.haml_spec.rb b/spec/views/commencer/show.html.haml_spec.rb index faedd3bcf..40010ea92 100644 --- a/spec/views/commencer/show.html.haml_spec.rb +++ b/spec/views/commencer/show.html.haml_spec.rb @@ -5,6 +5,7 @@ RSpec.describe 'commencer/show.html.haml', type: :view do before do assign(:procedure, procedure) + assign(:revision, procedure.published_revision) if user sign_in user end diff --git a/spec/views/new_administrateur/procedures/show.html.haml_spec.rb b/spec/views/new_administrateur/procedures/show.html.haml_spec.rb index 9ce7a8dd9..7901c610c 100644 --- a/spec/views/new_administrateur/procedures/show.html.haml_spec.rb +++ b/spec/views/new_administrateur/procedures/show.html.haml_spec.rb @@ -5,6 +5,7 @@ describe 'new_administrateur/procedures/show.html.haml', type: :view do before do assign(:procedure, procedure) assign(:procedure_lien, commencer_url(path: procedure.path)) + assign(:procedure_lien_test, commencer_test_url(path: procedure.path)) allow(view).to receive(:current_administrateur).and_return(procedure.administrateurs.first) end diff --git a/yarn.lock b/yarn.lock index 93c0ca786..9205dd177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,6 +1005,11 @@ enabled "2.0.x" kuler "^2.0.0" +"@headlessui/react@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.3.0.tgz#62287c92604923e5dfb394483e5ec2463e1baea6" + integrity sha512-2gqTO6BQ3Jr8vDX1B67n1gl6MGKTt6DBmR+H0qxwj0gTMnR2+Qpktj8alRWxsZBODyOiBb77QSQpE/6gG3MX4Q== + "@heroicons/react@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd" @@ -1567,6 +1572,11 @@ dependencies: "@types/node" ">= 8" +"@popperjs/core@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" + integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== + "@rails/actiontext@^6.0.3": version "6.0.3" resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-6.0.3.tgz#71dacd49df7c16a363e20aa89a53efcad8fcecde" @@ -1638,6 +1648,14 @@ "@reach/utils" "0.13.0" tslib "^2.0.0" +"@reach/auto-id@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.15.0.tgz#f46afebfc140b2099b95c7aec1f049d167d3833d" + integrity sha512-iACaCcZeqAhDf4OOwJpmHHS/LaRj9z3Ip8JmlhpCrFWV2YOIiiZk42amlBZX6CKH66Md+eriYZQk3TyAjk6Oxg== + dependencies: + "@reach/utils" "0.15.0" + tslib "^2.1.0" + "@reach/combobox@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.0.tgz#16bbdae42fc84f28025e6aa8552f1035cb15a365" @@ -1694,6 +1712,17 @@ prop-types "^15.7.2" tslib "^2.0.0" +"@reach/slider@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@reach/slider/-/slider-0.15.0.tgz#6b871e913bf10aa8f1ee5a4090478deff71d9c39" + integrity sha512-8dYJiclMpjHJ/wc0pIUxbwf8d3oO9ojnbMO31HPdqCqa+2KK8MoYzVdD32VsEa+gznHjoG9c05PPsrAjQd1yYg== + dependencies: + "@reach/auto-id" "0.15.0" + "@reach/utils" "0.15.0" + prop-types "^15.7.2" + tiny-warning "^1.0.3" + tslib "^2.1.0" + "@reach/utils@0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.13.0.tgz#2da775a910d8894bb34e1e94fe95842674f71844" @@ -1703,6 +1732,14 @@ tslib "^2.0.0" warning "^4.0.3" +"@reach/utils@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.15.0.tgz#5b183d668f9bb900b2dec7a33c028a2a828d27b2" + integrity sha512-JHHN7T5ucFiuQbqkgv8ECbRWKfRiJxrO/xHR3fHf+f2C7mVs/KkJHhYtovS1iEapR4silygX9PY0+QUmHPOTYw== + dependencies: + tiny-warning "^1.0.3" + tslib "^2.1.0" + "@sentry/browser@^5.15.5": version "5.15.5" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.5.tgz#d9a51f1388581067b50d30ed9b1aed2cbb333a36" @@ -10436,6 +10473,11 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" +react-fast-compare@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-intersection-observer@^8.31.0: version "8.31.0" resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605" @@ -10462,6 +10504,14 @@ react-mapbox-gl@^5.1.1: deep-equal "1.0.1" supercluster "^7.0.0" +react-popper@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-query@^3.9.7: version "3.9.7" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e" @@ -12052,6 +12102,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" @@ -12208,6 +12263,11 @@ 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: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -12680,7 +12740,7 @@ wait-port@^0.2.2: commander "^3.0.2" debug "^4.1.1" -warning@^4.0.3: +warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==