diff --git a/.eslintrc.js b/.eslintrc.js
index e9ea62e88..cc5432980 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -23,6 +23,7 @@ module.exports = {
rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'error',
'react/prop-types': 'off'
},
settings: {
@@ -51,7 +52,13 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
- ]
+ ],
+ rules: {
+ 'prettier/prettier': 'error',
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'error',
+ '@typescript-eslint/no-explicit-any': 'error'
+ }
}
]
};
diff --git a/.github/actions/ci-setup-rails/action.yml b/.github/actions/ci-setup-rails/action.yml
index 7f4b9edb8..15615d7e9 100644
--- a/.github/actions/ci-setup-rails/action.yml
+++ b/.github/actions/ci-setup-rails/action.yml
@@ -12,11 +12,12 @@ runs:
- name: Setup Node
uses: actions/setup-node@v2
with:
- node-version: '14'
cache: 'yarn'
- name: Install Node modules
- run: yarn install --frozen-lockfile
+ run: |
+ node --version
+ yarn install --frozen-lockfile
shell: bash
- name: Setup environment variables
diff --git a/.node-version b/.node-version
new file mode 100644
index 000000000..832d38506
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+16.14.0
diff --git a/Gemfile b/Gemfile
index b56231cea..0a5ce4a84 100644
--- a/Gemfile
+++ b/Gemfile
@@ -83,7 +83,7 @@ gem 'spreadsheet_architect'
gem 'typhoeus'
gem 'warden'
gem 'webpacker'
-gem 'zipline', github: 'fringd/zipline', ref: 'd637bbff2' # Unreleased 1.3.0, with a fix for Ruby 3.0 kwargs
+gem 'zipline'
gem 'zxcvbn-ruby', require: 'zxcvbn'
group :test do
@@ -120,7 +120,7 @@ end
group :development, :test do
gem 'graphql-schema_comparator'
- gem 'mina', git: 'https://github.com/mina-deploy/mina.git', require: false # Deploy
+ gem 'mina', require: false # Deploy
gem 'pry-byebug' # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'rspec-rails'
gem 'simple_xlsx_reader'
diff --git a/Gemfile.lock b/Gemfile.lock
index 74daed95a..50bdda94b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,60 +1,43 @@
-GIT
- remote: https://github.com/fringd/zipline.git
- revision: d637bbff262f59718d23a65f50b50163b8ba749f
- ref: d637bbff2
- specs:
- zipline (1.3.0)
- actionpack (>= 3.2.1, < 7.0)
- zip_tricks (>= 4.2.1, < 6.0)
-
-GIT
- remote: https://github.com/mina-deploy/mina.git
- revision: 84fa84c7f7f94f9518ef9b7099396ab6676b5881
- specs:
- mina (1.2.3)
- open4 (~> 1.3.4)
- rake
-
GEM
remote: https://rubygems.org/
specs:
aasm (5.2.0)
concurrent-ruby (~> 1.0)
acsv (0.0.1)
- actioncable (6.1.4.4)
- actionpack (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actioncable (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.4.4)
- actionpack (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionmailbox (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ activejob (= 6.1.4.6)
+ activerecord (= 6.1.4.6)
+ activestorage (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
mail (>= 2.7.1)
- actionmailer (6.1.4.4)
- actionpack (= 6.1.4.4)
- actionview (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionmailer (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ actionview (= 6.1.4.6)
+ activejob (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.1.4.4)
- actionview (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionpack (6.1.4.6)
+ actionview (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.4.4)
- actionpack (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actiontext (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ activerecord (= 6.1.4.6)
+ activestorage (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
nokogiri (>= 1.8.5)
- actionview (6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionview (6.1.4.6)
+ activesupport (= 6.1.4.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -72,26 +55,26 @@ GEM
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
- activejob (6.1.4.4)
- activesupport (= 6.1.4.4)
+ activejob (6.1.4.6)
+ activesupport (= 6.1.4.6)
globalid (>= 0.3.6)
- activemodel (6.1.4.4)
- activesupport (= 6.1.4.4)
- activerecord (6.1.4.4)
- activemodel (= 6.1.4.4)
- activesupport (= 6.1.4.4)
- activestorage (6.1.4.4)
- actionpack (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ activemodel (6.1.4.6)
+ activesupport (= 6.1.4.6)
+ activerecord (6.1.4.6)
+ activemodel (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
+ activestorage (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ activejob (= 6.1.4.6)
+ activerecord (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
activestorage-openstack (1.5.1)
fog-openstack (~> 1.0)
marcel
rails (>= 5.2.2)
- activesupport (6.1.4.4)
+ activesupport (6.1.4.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -183,6 +166,7 @@ GEM
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.9)
connection_pool (2.2.3)
+ content_disposition (1.0.0)
crack (0.4.5)
rexml
crass (1.0.6)
@@ -351,7 +335,7 @@ GEM
domain_name (~> 0.5)
http_accept_language (2.1.1)
httpclient (2.8.3)
- i18n (1.8.11)
+ i18n (1.10.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.33)
activesupport (>= 4.0.2)
@@ -417,7 +401,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
- loofah (2.13.0)
+ loofah (2.14.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -431,9 +415,12 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0212)
+ mina (1.2.4)
+ open4 (~> 1.3.4)
+ rake
mini_magick (4.11.0)
mini_mime (1.1.2)
- mini_portile2 (2.6.1)
+ mini_portile2 (2.7.1)
minitest (5.15.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
@@ -444,8 +431,8 @@ GEM
ruby2_keywords (~> 0.0.1)
netrc (0.11.0)
nio4r (2.5.8)
- nokogiri (1.12.5)
- mini_portile2 (~> 2.6.1)
+ nokogiri (1.13.1)
+ mini_portile2 (~> 2.7.0)
racc (~> 1.4)
open4 (1.3.4)
openid_connect (1.3.0)
@@ -515,20 +502,20 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.1.4.4)
- actioncable (= 6.1.4.4)
- actionmailbox (= 6.1.4.4)
- actionmailer (= 6.1.4.4)
- actionpack (= 6.1.4.4)
- actiontext (= 6.1.4.4)
- actionview (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activemodel (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ rails (6.1.4.6)
+ actioncable (= 6.1.4.6)
+ actionmailbox (= 6.1.4.6)
+ actionmailer (= 6.1.4.6)
+ actionpack (= 6.1.4.6)
+ actiontext (= 6.1.4.6)
+ actionview (= 6.1.4.6)
+ activejob (= 6.1.4.6)
+ activemodel (= 6.1.4.6)
+ activerecord (= 6.1.4.6)
+ activestorage (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
bundler (>= 1.15.0)
- railties (= 6.1.4.4)
+ railties (= 6.1.4.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@@ -547,9 +534,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
- railties (6.1.4.4)
- actionpack (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ railties (6.1.4.6)
+ actionpack (= 6.1.4.6)
+ activesupport (= 6.1.4.6)
method_source
rake (>= 0.13)
thor (~> 1.0)
@@ -768,8 +755,12 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.5.3)
+ zeitwerk (2.5.4)
zip_tricks (5.6.0)
+ zipline (1.4.1)
+ actionpack (>= 6.0, < 8.0)
+ content_disposition (~> 1.0)
+ zip_tricks (>= 4.2.1, < 6.0)
zxcvbn-ruby (1.2.0)
PLATFORMS
@@ -842,7 +833,7 @@ DEPENDENCIES
lograge
logstash-event
mailjet
- mina!
+ mina
openid_connect
pg
phonelib
@@ -892,7 +883,7 @@ DEPENDENCIES
webdrivers (~> 4.0)
webmock
webpacker
- zipline!
+ zipline
zxcvbn-ruby
BUNDLED WITH
diff --git a/app/assets/images/login-with-fc-hover.svg b/app/assets/images/login-with-fc-hover.svg
index 2a580527e..5a2d16909 100644
--- a/app/assets/images/login-with-fc-hover.svg
+++ b/app/assets/images/login-with-fc-hover.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/assets/images/login-with-fc.svg b/app/assets/images/login-with-fc.svg
index afc1e8d89..3a95ff212 100644
--- a/app/assets/images/login-with-fc.svg
+++ b/app/assets/images/login-with-fc.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/assets/stylesheets/agentconnect.scss b/app/assets/stylesheets/agentconnect.scss
new file mode 100644
index 000000000..350f85d52
--- /dev/null
+++ b/app/assets/stylesheets/agentconnect.scss
@@ -0,0 +1,27 @@
+@import "colors";
+@import "constants";
+
+#agentconnect {
+ .agent {
+ color: $blue-france-500;
+ text-align: center;
+ font-size: 28px;
+ font-weight: bold;
+ }
+
+ .box {
+ background-color: #F2F2F9;
+ padding: $default-padding;
+
+ ul {
+ list-style: disc;
+ padding-left: inherit;
+ }
+ }
+
+ .citizen {
+ font-size: 16px;
+ color: $blue-france-500;
+ font-weight: bold;
+ }
+}
diff --git a/app/assets/stylesheets/auth.scss b/app/assets/stylesheets/auth.scss
index 4cc36d96e..410771461 100644
--- a/app/assets/stylesheets/auth.scss
+++ b/app/assets/stylesheets/auth.scss
@@ -3,17 +3,23 @@
@import "placeholders";
@import "mixins";
-#auth {
+#auth,
+#agentconnect {
// On small screens, hide the procedure description text on auth pages.
// It avoids pushing the sign-in/sign-up form out of the viewport.
//
// The procedure description can still be read from the /commencer
// pages.
@media (max-width: $two-columns-breakpoint) {
- .procedure-preview {
+ .procedure-preview,
+ .agent-intro {
display: none;
}
}
+
+ .column {
+ padding-top: 2 * $default-spacer;
+ }
}
.auth-form {
@@ -53,6 +59,10 @@
}
.sign-in-form .form {
+ input[type="email"] {
+ margin-bottom: $default-padding;
+ }
+
input[type="password"] {
margin-bottom: $default-spacer;
}
@@ -61,3 +71,10 @@
margin-bottom: 0;
}
}
+
+#session-new {
+ .important-header {
+ font-weight: bold;
+ font-size: 18px;
+ }
+}
diff --git a/app/assets/stylesheets/france-connect-login.scss b/app/assets/stylesheets/france-connect-login.scss
index f54289631..20c66f404 100644
--- a/app/assets/stylesheets/france-connect-login.scss
+++ b/app/assets/stylesheets/france-connect-login.scss
@@ -15,8 +15,8 @@
.france-connect-login-button {
display: inline-block;
- height: 52px;
- width: 186px;
+ height: 60px;
+ width: 230px;
margin: auto;
margin-bottom: 8px;
background-image: image-url("login-with-fc.svg"), image-url("login-with-fc-hover.svg");
diff --git a/app/assets/stylesheets/helpers.scss b/app/assets/stylesheets/helpers.scss
deleted file mode 100644
index 4577ef84c..000000000
--- a/app/assets/stylesheets/helpers.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-@import "constants";
-
-.m-0 {
- margin: 0px !important;
-}
-
-.mb-1 {
- margin-bottom: $default-spacer;
-}
-
-.mr-1 {
- margin-right: $default-spacer !important;
-}
-
-.mb-4 {
- margin-bottom: 4 * $default-spacer !important;
-}
-
-.ml-1 {
- margin-left: $default-spacer;
-}
-
-.pl-0 {
- padding-left: 0px !important;
-}
-
-.p-0 {
- padding: 0px !important;
-}
-
-.bold {
- font-weight: bold;
-}
-
-.numbers-delimiter {
- display: inline-block;
- width: 5px;
-}
-
-.text-center {
- text-align: center;
-}
diff --git a/app/assets/stylesheets/merci.scss b/app/assets/stylesheets/merci.scss
index e52b91bb2..b40f676a2 100644
--- a/app/assets/stylesheets/merci.scss
+++ b/app/assets/stylesheets/merci.scss
@@ -2,38 +2,8 @@
@import "common";
@import "constants";
-.merci {
- text-align: center;
- margin-bottom: 60px;
-
+.merci .monavis {
img {
- margin-top: 4 * $default-padding;
- }
-
- h1 {
- margin: (2 * $default-padding) 0;
- }
-
- b {
- font-weight: bold;
- }
-
- .send {
- margin-bottom: 2 * $default-padding;
- font-size: 20px;
- }
-
- p {
- margin: $default-padding;
- }
-
- a {
- margin-top: 40px;
- }
-
- .monavis {
- img {
- margin-top: 2 * $default-padding;
- }
+ margin-top: 2 * $default-padding;
}
}
diff --git a/app/assets/stylesheets/procedure_context.scss b/app/assets/stylesheets/procedure_context.scss
index 5cc67ad4d..24f600222 100644
--- a/app/assets/stylesheets/procedure_context.scss
+++ b/app/assets/stylesheets/procedure_context.scss
@@ -22,6 +22,12 @@ $procedure-description-line-height: 22px;
font-weight: bold;
}
+ .small-simple {
+ font-size: 16px;
+ color: $blue-france-500;
+ font-weight: bold;
+ }
+
.close-procedure {
font-size: 12px;
}
diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss
index 535217901..14e90df81 100644
--- a/app/assets/stylesheets/utils.scss
+++ b/app/assets/stylesheets/utils.scss
@@ -1,6 +1,7 @@
@import "colors";
@import "constants";
+// floats
.pull-left {
float: left;
}
@@ -13,6 +14,9 @@
clear: both;
}
+
+// text
+.text-center,
.center {
text-align: center;
}
@@ -21,12 +25,21 @@
text-align: right;
}
-.hidden {
- display: none;
+.text-sm {
+ font-size: 14px;
}
-.width-100 {
- width: 100%;
+.text-lg {
+ font-size: 18px;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.numbers-delimiter {
+ display: inline-block;
+ width: 5px;
}
.empty-text {
@@ -45,95 +58,42 @@
}
}
+// display
+.hidden {
+ display: none;
+}
+
+// sizing
+.width-100 {
+ width: 100%;
+}
+
+// who known
.highlighted {
background: $orange-bg;
color: $black;
}
-.text-sm {
- font-size: 14px;
-}
+// generate spacer utility like bootstrap my-2 -> margin-left/right: 2 * $default-spacer
+// using $direction.key as css modifier, $direction.values to set css properties
+// scale it using $steps
+$directions: (
+ "t": ("margin-top"),
+ "r": ("margin-right"),
+ "b": ("margin-bottom"),
+ "l": ("margin-left"),
+ "x": ("margin-left", "margin-right"),
+ "y": ("margin-top", "margin-bottom"),
+ "": ("margin")
+);
+$steps: (0, 1, 2, 3, 4, 5, 6, 7, 8);
-.text-lg {
- font-size: 18px;
-}
-
-.mt-1 {
- margin-top: $default-spacer;
-}
-
-.mt-2 {
- margin-top: 2 * $default-spacer;
-}
-
-.mt-3 {
- margin-top: 3 * $default-spacer;
-}
-
-.mt-4 {
- margin-top: 4 * $default-spacer;
-}
-
-.mt-8 {
- margin-top: 8 * $default-spacer;
-}
-
-.mb-1 {
- margin-bottom: $default-spacer;
-}
-
-.mb-2 {
- margin-bottom: 2 * $default-spacer;
-}
-
-.mb-3 {
- margin-bottom: 3 * $default-spacer;
-}
-
-.mb-4 {
- margin-bottom: 4 * $default-spacer;
-}
-
-.mb-8 {
- margin-bottom: 8 * $default-spacer;
-}
-
-.pt-1 {
- padding-top: $default-spacer;
-}
-
-.pt-2 {
- padding-top: 2 * $default-spacer;
-}
-
-.pt-3 {
- padding-top: 3 * $default-spacer;
-}
-
-.pt-4 {
- padding-top: 4 * $default-spacer;
-}
-
-.pt-8 {
- padding-top: 8 * $default-spacer;
-}
-
-.pb-1 {
- padding-bottom: $default-spacer;
-}
-
-.pb-2 {
- padding-bottom: 2 * $default-spacer;
-}
-
-.pb-3 {
- padding-bottom: 3 * $default-spacer;
-}
-
-.pb-4 {
- padding-bottom: 4 * $default-spacer;
-}
-
-.pb-8 {
- padding-bottom: 8 * $default-spacer;
+@each $modifier, $properties in $directions {
+ @each $step in $steps {
+ @each $property in $properties {
+ .m#{$modifier}-#{$step} {
+ #{$property}: $step * $default-spacer;
+ }
+ }
+ }
}
diff --git a/app/controllers/administrateurs/dossier_submitted_messages_controller.rb b/app/controllers/administrateurs/dossier_submitted_messages_controller.rb
new file mode 100644
index 000000000..21cc6bb77
--- /dev/null
+++ b/app/controllers/administrateurs/dossier_submitted_messages_controller.rb
@@ -0,0 +1,45 @@
+module Administrateurs
+ class DossierSubmittedMessagesController < AdministrateurController
+ before_action :retrieve_procedure
+
+ def edit
+ @dossier_submitted_message = build_dossier_submitted_message
+ end
+
+ def update
+ @dossier_submitted_message = build_dossier_submitted_message(dossier_submitted_message_params)
+
+ if @dossier_submitted_message.save
+ redirect_to admin_procedure_path(@procedure), flash: { notice: "Les informations de fin de dépot ont bien été sauvegardées." }
+ else
+ flash.alert = "Impossible de sauvegarder les informations de fin de dépot, veuillez ré-essayer."
+ render :edit, status: 400
+ end
+ end
+
+ def create
+ @dossier_submitted_message = build_dossier_submitted_message(dossier_submitted_message_params)
+ if @dossier_submitted_message.save
+ redirect_to admin_procedure_path(@procedure), flash: { notice: "Les informations de fin de dépot ont bien été sauvegardées." }
+ else
+ flash.alert = "Impossible de sauvegarder les informations de \"fin de dépot\", veuillez ré-essayer."
+ render :edit, status: 400
+ end
+ end
+
+ private
+
+ # for now, only works on active revision no matter the procedure_revision_policy
+ def build_dossier_submitted_message(attributes = {})
+ dossier_submitted_message = @procedure.active_revision.dossier_submitted_message || @procedure.active_revision.build_dossier_submitted_message
+
+ dossier_submitted_message.attributes = attributes unless attributes.empty?
+ dossier_submitted_message
+ end
+
+ def dossier_submitted_message_params
+ params.require(:dossier_submitted_message)
+ .permit(:message_on_submit_by_usager)
+ end
+ end
+end
diff --git a/app/controllers/champs/siret_controller.rb b/app/controllers/champs/siret_controller.rb
index fff8d2e97..0d790704a 100644
--- a/app/controllers/champs/siret_controller.rb
+++ b/app/controllers/champs/siret_controller.rb
@@ -17,7 +17,7 @@ class Champs::SiretController < ApplicationController
begin
etablissement = find_etablissement_with_siret
- rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::ServiceUnavailable
+ rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut, APIEntreprise::API::Error::ServiceUnavailable
# i18n-tasks-use t('errors.messages.siret_network_error')
return siret_error(:network_error)
end
diff --git a/app/controllers/experts/avis_controller.rb b/app/controllers/experts/avis_controller.rb
index c856fe2eb..393a18fae 100644
--- a/app/controllers/experts/avis_controller.rb
+++ b/app/controllers/experts/avis_controller.rb
@@ -17,7 +17,8 @@ module Experts
end
def procedure
- @procedure = Procedure.find(params[:procedure_id])
+ @procedure = current_expert.procedures.find_by(id: params[:procedure_id])
+ redirect_to(expert_all_avis_path, flash: { alert: "Vous n’avez pas accès à cette démarche." }) and return unless @procedure
expert_avis = current_expert.avis.includes(:dossier).not_hidden_by_administration.where(dossiers: { groupe_instructeur: GroupeInstructeur.where(procedure: @procedure.id) })
@avis_a_donner = expert_avis.without_answer
@avis_donnes = expert_avis.with_answer
@@ -156,7 +157,8 @@ module Experts
end
def set_avis_and_dossier
- @avis = Avis.find(params[:id])
+ @avis = current_expert.avis.find_by(id: params[:id])
+ redirect_to(expert_all_avis_path, flash: { alert: "Vous n’avez pas accès à cet avis." }) and return unless @avis
@dossier = @avis.dossier
end
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index 2b059e156..89a665419 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -106,7 +106,7 @@ module Users
sanitized_siret = siret_model.siret
begin
etablissement = APIEntrepriseService.create_etablissement(@dossier, sanitized_siret, current_user.id)
- rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut
+ rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut, APIEntreprise::API::Error::ServiceUnavailable
return render_siret_error(t('errors.messages.siret_network_error'))
end
if etablissement.nil?
diff --git a/app/javascript/components/ComboMultiple.jsx b/app/javascript/components/ComboMultiple.jsx
index f2eb93275..7938518a0 100644
--- a/app/javascript/components/ComboMultiple.jsx
+++ b/app/javascript/components/ComboMultiple.jsx
@@ -26,6 +26,19 @@ import { useDeferredSubmit, useHiddenField } from './shared/hooks';
const Context = createContext();
+const optionValueByLabel = (values, options, label) => {
+ const maybeOption = values.includes(label)
+ ? [label, label]
+ : options.find(([optionLabel]) => optionLabel == label);
+ return maybeOption ? maybeOption[1] : undefined;
+};
+const optionLabelByValue = (values, options, value) => {
+ const maybeOption = values.includes(value)
+ ? [value, value]
+ : options.find(([, optionValue]) => optionValue == value);
+ return maybeOption ? maybeOption[0] : undefined;
+};
+
function ComboMultiple({
options,
id,
@@ -40,9 +53,6 @@ function ComboMultiple({
invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
invariant(group, 'ComboMultiple: `group` is required');
- if (!Array.isArray(options[0])) {
- options = options.filter((o) => o).map((o) => [o, o]);
- }
const inputRef = useRef();
const [term, setTerm] = useState('');
const [selections, setSelections] = useState(selected);
@@ -51,25 +61,22 @@ function ComboMultiple({
const removedLabelledby = `${inputId}-remove`;
const selectedLabelledby = `${inputId}-selected`;
- const optionValueByLabel = (label) => {
- const maybeOption = newValues.includes(label)
- ? [label, label]
- : options.find(([optionLabel]) => optionLabel == label);
- return maybeOption ? maybeOption[1] : undefined;
- };
- const optionLabelByValue = (value) => {
- const maybeOption = newValues.includes(value)
- ? [value, value]
- : options.find(([, optionValue]) => optionValue == value);
- return maybeOption ? maybeOption[0] : undefined;
- };
-
+ const optionsWithLabels = useMemo(
+ () =>
+ Array.isArray(options[0])
+ ? options
+ : options.filter((o) => o).map((o) => [o, o]),
+ [options]
+ );
const extraOptions = useMemo(
() =>
- acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
+ acceptNewValues &&
+ term &&
+ term.length > 2 &&
+ !optionLabelByValue(newValues, optionsWithLabels, term)
? [[term, term]]
: [],
- [acceptNewValues, term, newValues.join(',')]
+ [acceptNewValues, term, optionsWithLabels, newValues]
);
const results = useMemo(
() =>
@@ -77,12 +84,12 @@ function ComboMultiple({
...extraOptions,
...(term
? matchSorter(
- options.filter(([label]) => !label.startsWith('--')),
+ optionsWithLabels.filter(([label]) => !label.startsWith('--')),
term
)
- : options)
+ : optionsWithLabels)
].filter(([, value]) => !selections.includes(value)),
- [term, selections.join(','), newValues.join(',')]
+ [term, selections, extraOptions, optionsWithLabels]
);
const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
const awaitFormSubmit = useDeferredSubmit(hiddenField);
@@ -100,7 +107,7 @@ function ComboMultiple({
};
const onSelect = (value) => {
- const maybeValue = [...extraOptions, ...options].find(
+ const maybeValue = [...extraOptions, ...optionsWithLabels].find(
([val]) => val == value
);
const selectedValue = maybeValue && maybeValue[1];
@@ -128,7 +135,7 @@ function ComboMultiple({
};
const onRemove = (label) => {
- const optionValue = optionValueByLabel(label);
+ const optionValue = optionValueByLabel(newValues, options, label);
if (optionValue) {
saveSelection((selections) =>
selections.filter((value) => value != optionValue)
@@ -149,7 +156,9 @@ function ComboMultiple({
) {
if (
term &&
- [...extraOptions, ...options].map(([label]) => label).includes(term)
+ [...extraOptions, ...optionsWithLabels]
+ .map(([label]) => label)
+ .includes(term)
) {
event.preventDefault();
onSelect(term);
@@ -172,7 +181,9 @@ function ComboMultiple({
const onBlur = () => {
const shouldSelect =
term &&
- [...extraOptions, ...options].map(([label]) => label).includes(term);
+ [...extraOptions, ...optionsWithLabels]
+ .map(([label]) => label)
+ .includes(term);
awaitFormSubmit(() => {
if (shouldSelect) {
@@ -199,7 +210,7 @@ function ComboMultiple({
))}
diff --git a/app/javascript/components/ComboSearch.tsx b/app/javascript/components/ComboSearch.tsx
index 4e322c453..9bdedc397 100644
--- a/app/javascript/components/ComboSearch.tsx
+++ b/app/javascript/components/ComboSearch.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useRef } from 'react';
+import React, { useState, useRef, ChangeEventHandler } from 'react';
import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query';
import {
@@ -68,7 +68,7 @@ function ComboSearch({
const [, value, label] = transformResult(result);
return label ?? value;
};
- const setExternalValueAndId = useCallback((label: string) => {
+ const setExternalValueAndId = (label: string) => {
const { key, value, result } = resultsMap.current[label];
if (onChange) {
onChange(value, result);
@@ -76,36 +76,35 @@ function ComboSearch({
setExternalId(key);
setExternalValue(value);
}
- }, []);
+ };
const awaitFormSubmit = useDeferredSubmit(hiddenField);
- const handleOnChange = useCallback(
- ({ target: { value } }) => {
- setValue(value);
- if (!value) {
- if (onChange) {
- onChange(null);
- } else {
- setExternalId('');
- setExternalValue('');
- }
- } else if (value.length >= minimumInputLength) {
- setSearchTerm(value.trim());
- if (allowInputValues) {
- setExternalId('');
- setExternalValue(value);
- }
+ const handleOnChange: ChangeEventHandler = ({
+ target: { value }
+ }) => {
+ setValue(value);
+ if (!value) {
+ if (onChange) {
+ onChange(null);
+ } else {
+ setExternalId('');
+ setExternalValue('');
}
- },
- [minimumInputLength]
- );
+ } else if (value.length >= minimumInputLength) {
+ setSearchTerm(value.trim());
+ if (allowInputValues) {
+ setExternalId('');
+ setExternalValue(value);
+ }
+ }
+ };
- const handleOnSelect = useCallback((value: string) => {
+ const handleOnSelect = (value: string) => {
setExternalValueAndId(value);
setValue(value);
setSearchTerm('');
awaitFormSubmit.done();
- }, []);
+ };
const { isSuccess, data } = useQuery(
[scope, debouncedSearchTerm, scopeExtra],
@@ -117,14 +116,14 @@ function ComboSearch({
const results =
isSuccess && data ? transformResults(debouncedSearchTerm, data) : [];
- const onBlur = useCallback(() => {
+ const onBlur = () => {
if (!allowInputValues && isSuccess && results[0]) {
const label = getLabel(results[0]);
awaitFormSubmit(() => {
handleOnSelect(label);
});
}
- }, [data]);
+ };
return (
diff --git a/app/javascript/components/MapEditor/components/CadastreLayer.tsx b/app/javascript/components/MapEditor/components/CadastreLayer.tsx
index 4ebb14523..f2f0b7086 100644
--- a/app/javascript/components/MapEditor/components/CadastreLayer.tsx
+++ b/app/javascript/components/MapEditor/components/CadastreLayer.tsx
@@ -28,35 +28,41 @@ export function CadastreLayer({
const map = useMapLibre();
const selectedCadastresRef = useRef(new Set());
- const highlightFeature = useCallback((cid: string, highlight: boolean) => {
- if (highlight) {
- selectedCadastresRef.current.add(cid);
- } else {
- selectedCadastresRef.current.delete(cid);
- }
- if (selectedCadastresRef.current.size == 0) {
- map.setFilter('parcelle-highlighted', ['in', 'id', '']);
- } else {
- map.setFilter('parcelle-highlighted', [
- 'in',
- 'id',
- ...selectedCadastresRef.current
- ]);
- }
- }, []);
+ const highlightFeature = useCallback(
+ (cid: string, highlight: boolean) => {
+ if (highlight) {
+ selectedCadastresRef.current.add(cid);
+ } else {
+ selectedCadastresRef.current.delete(cid);
+ }
+ if (selectedCadastresRef.current.size == 0) {
+ map.setFilter('parcelle-highlighted', ['in', 'id', '']);
+ } else {
+ map.setFilter('parcelle-highlighted', [
+ 'in',
+ 'id',
+ ...selectedCadastresRef.current
+ ]);
+ }
+ },
+ [map]
+ );
- const hoverFeature = useCallback((feature: Feature, hover: boolean) => {
- if (!selectedCadastresRef.current.has(feature.properties?.id)) {
- map.setFeatureState(
- {
- source: 'cadastre',
- sourceLayer: 'parcelles',
- id: String(feature.id)
- },
- { hover }
- );
- }
- }, []);
+ const hoverFeature = useCallback(
+ (feature: Feature, hover: boolean) => {
+ if (!selectedCadastresRef.current.has(feature.properties?.id)) {
+ map.setFeatureState(
+ {
+ source: 'cadastre',
+ sourceLayer: 'parcelles',
+ id: String(feature.id)
+ },
+ { hover }
+ );
+ }
+ },
+ [map]
+ );
useCadastres(featureCollection, {
hoverFeature,
diff --git a/app/javascript/components/MapEditor/components/DrawLayer.tsx b/app/javascript/components/MapEditor/components/DrawLayer.tsx
index f5292da9a..9bdeb3a2a 100644
--- a/app/javascript/components/MapEditor/components/DrawLayer.tsx
+++ b/app/javascript/components/MapEditor/components/DrawLayer.tsx
@@ -48,6 +48,8 @@ export function DrawLayer({
trash: true
}
});
+ // We use mapbox-draw plugin with maplibre. They are compatible but types are not.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
map.addControl(draw as any, 'top-left');
draw.set(
filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR)
@@ -64,11 +66,15 @@ export function DrawLayer({
return () => {
if (drawRef.current) {
+ // We use mapbox-draw plugin with maplibre. They are compatible but types are not.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
map.removeControl(drawRef.current as any);
drawRef.current = null;
}
};
- }, [enabled]);
+ // We only want to rerender draw layer on component mount or when the layer is toggled.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [map, enabled]);
const onSetId = useCallback(({ detail }) => {
drawRef.current?.setFeatureProperty(detail.lid, 'id', detail.id);
@@ -167,7 +173,9 @@ function useExternalEvents(
useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike);
- }, []);
+ // We only want to zoom on bbox on component mount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus);
useEvent('map:feature:create', onFeatureCreate);
diff --git a/app/javascript/components/MapReader/components/GeoJSONLayer.tsx b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx
index 956a71fc4..f7161d00d 100644
--- a/app/javascript/components/MapReader/components/GeoJSONLayer.tsx
+++ b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx
@@ -44,13 +44,13 @@ export function GeoJSONLayer({
popup.remove();
}
},
- [popup]
+ [map, popup]
);
const onMouseLeave = useCallback(() => {
map.getCanvas().style.cursor = '';
popup.remove();
- }, [popup]);
+ }, [map, popup]);
useExternalEvents(featureCollection);
@@ -99,17 +99,22 @@ export function GeoJSONLayer({
function useExternalEvents(featureCollection: FeatureCollection) {
const fitBounds = useFitBounds();
- const onFeatureFocus = useCallback(({ detail }) => {
- const { id } = detail;
- const feature = findFeature(featureCollection, id);
- if (feature) {
- fitBounds(getBounds(feature.geometry));
- }
- }, []);
+ const onFeatureFocus = useCallback(
+ ({ detail }) => {
+ const { id } = detail;
+ const feature = findFeature(featureCollection, id);
+ if (feature) {
+ fitBounds(getBounds(feature.geometry));
+ }
+ },
+ [featureCollection, fitBounds]
+ );
useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike);
- }, []);
+ // We only want to zoom on bbox on component mount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus);
}
@@ -139,7 +144,7 @@ function LineStringLayer({
type: 'line',
paint: lineStringSelectionLine
});
- }, []);
+ }, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
@@ -172,7 +177,7 @@ function PointLayer({
type: 'circle',
paint: pointSelectionCircle
});
- }, []);
+ }, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
@@ -212,7 +217,7 @@ function PolygonLayer({
type: 'fill',
paint: polygonSelectionFill
});
- }, []);
+ }, [map, layerId, lineLayerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
diff --git a/app/javascript/components/shared/FlashMessage.tsx b/app/javascript/components/shared/FlashMessage.tsx
index 6a092ff60..413eae83a 100644
--- a/app/javascript/components/shared/FlashMessage.tsx
+++ b/app/javascript/components/shared/FlashMessage.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { createPortal } from 'react-dom';
+import invariant from 'tiny-invariant';
export function FlashMessage({
message,
@@ -12,11 +13,13 @@ export function FlashMessage({
sticky?: boolean;
fixed?: boolean;
}) {
+ const element = document.getElementById('flash_messages');
+ invariant(element, 'Flash messages root element not found');
return createPortal(
,
- document.getElementById('flash_messages')!
+ element
);
}
diff --git a/app/javascript/components/shared/maplibre/MapLibre.tsx b/app/javascript/components/shared/maplibre/MapLibre.tsx
index b123733b2..2b439abfa 100644
--- a/app/javascript/components/shared/maplibre/MapLibre.tsx
+++ b/app/javascript/components/shared/maplibre/MapLibre.tsx
@@ -5,7 +5,8 @@ import React, {
useEffect,
useMemo,
ReactNode,
- createContext
+ createContext,
+ useCallback
} from 'react';
import maplibre, { Map, Style, NavigationControl } from 'maplibre-gl';
@@ -37,11 +38,14 @@ export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
const containerRef = useRef(null);
const [map, setMap] = useState