From ca55fc9c4015eeb132c08c4618aeae6315ec6c6f Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 10:23:57 +0200 Subject: [PATCH 01/39] style(demande): fix horizontal alignements en construction --- app/views/users/dossiers/demande.html.haml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/views/users/dossiers/demande.html.haml b/app/views/users/dossiers/demande.html.haml index 543b3c867..ef21d650e 100644 --- a/app/views/users/dossiers/demande.html.haml +++ b/app/views/users/dossiers/demande.html.haml @@ -6,20 +6,15 @@ .dossier-container.fr-mb-4w = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } - - if @dossier.en_construction? - .fr-container - .fr-grid-row.fr-grid-row--center - .fr-col-xl-10 - = render Dossiers::EnConstructionNotSubmittedComponent.new(dossier: @dossier, user: current_user) .fr-container .fr-grid-row.fr-grid-row--center .fr-col-md-9 + - if @dossier.en_construction? + = render Dossiers::EnConstructionNotSubmittedComponent.new(dossier: @dossier, user: current_user) + = render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' } - - - if !@dossier.read_only? - .fr-container.fr-mt-2w - .fr-grid-row.fr-grid-row--center - .fr-col-xl-8.fr-col-offset-xl-2 - %p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm', - title: t('views.users.dossiers.demande.edit_dossier_title') + - if !@dossier.read_only? + .fr-px-2w.fr-mt-2w + %p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm', + title: t('views.users.dossiers.demande.edit_dossier_title') From 1eb0bdb4ae91822bf19125703d8b3f17e6b54174 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 11:04:16 +0200 Subject: [PATCH 02/39] chore: cookies with "secure" flag (only transmitted through https) --- app/controllers/agent_connect/agent_controller.rb | 4 ++-- app/controllers/application_controller.rb | 2 +- .../application_controller/long_lived_authenticity_token.rb | 3 ++- app/controllers/instructeurs/procedures_controller.rb | 3 ++- app/models/concerns/trusted_device_concern.rb | 3 ++- config/initializers/session_store.rb | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/controllers/agent_connect/agent_controller.rb b/app/controllers/agent_connect/agent_controller.rb index 6116b1f2b..cd42e6f16 100644 --- a/app/controllers/agent_connect/agent_controller.rb +++ b/app/controllers/agent_connect/agent_controller.rb @@ -12,8 +12,8 @@ class AgentConnect::AgentController < ApplicationController def login uri, state, nonce = AgentConnectService.authorization_uri - cookies.encrypted[STATE_COOKIE_NAME] = state - cookies.encrypted[NONCE_COOKIE_NAME] = nonce + cookies.encrypted[STATE_COOKIE_NAME] = { value: state, secure: Rails.env.production? } + cookies.encrypted[NONCE_COOKIE_NAME] = { value: nonce, secure: Rails.env.production? } redirect_to uri, allow_other_host: true end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bc172fe47..2771bf78f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base def set_locale(locale) if locale && locale.to_sym.in?(I18n.available_locales) - cookies[:locale] = locale + cookies[:locale] = { value: locale, secure: Rails.env.production? } if user_signed_in? current_user.update(locale: locale) end diff --git a/app/controllers/application_controller/long_lived_authenticity_token.rb b/app/controllers/application_controller/long_lived_authenticity_token.rb index cb10c52bd..54eb16f31 100644 --- a/app/controllers/application_controller/long_lived_authenticity_token.rb +++ b/app/controllers/application_controller/long_lived_authenticity_token.rb @@ -24,7 +24,8 @@ module ApplicationController::LongLivedAuthenticityToken cookies.signed[COOKIE_NAME] = { value: csrf_token, expires: 1.year.from_now, - httponly: true + httponly: true, + secure: Rails.env.production? } session[:_csrf_token] = csrf_token diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 2b8702cf3..6c8326050 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -248,7 +248,8 @@ module Instructeurs @export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur) cookies.encrypted[cookies_export_key] = { value: DateTime.current, - expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT + expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT, + secure: Rails.env.production? } respond_to do |format| diff --git a/app/models/concerns/trusted_device_concern.rb b/app/models/concerns/trusted_device_concern.rb index 2aa895893..1765f565c 100644 --- a/app/models/concerns/trusted_device_concern.rb +++ b/app/models/concerns/trusted_device_concern.rb @@ -8,7 +8,8 @@ module TrustedDeviceConcern cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = { value: JSON.generate({ created_at: start_at }), expires: start_at + TRUSTED_DEVICE_PERIOD, - httponly: true + httponly: true, + secure: Rails.env.production? } end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index f110f4cb3..96e2b62b5 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,3 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, key: '_DS_session' +Rails.application.config.session_store :cookie_store, key: '_DS_session', secure: Rails.env.production? From 990dfbcf9e3e10c6a4adb45a8519854030b344d8 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 11:54:10 +0200 Subject: [PATCH 03/39] chore(cookies): cookies http only --- app/controllers/agent_connect/agent_controller.rb | 4 ++-- app/controllers/application_controller.rb | 2 +- app/controllers/instructeurs/procedures_controller.rb | 1 + config/initializers/session_store.rb | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/agent_connect/agent_controller.rb b/app/controllers/agent_connect/agent_controller.rb index cd42e6f16..c8ab572e2 100644 --- a/app/controllers/agent_connect/agent_controller.rb +++ b/app/controllers/agent_connect/agent_controller.rb @@ -12,8 +12,8 @@ class AgentConnect::AgentController < ApplicationController def login uri, state, nonce = AgentConnectService.authorization_uri - cookies.encrypted[STATE_COOKIE_NAME] = { value: state, secure: Rails.env.production? } - cookies.encrypted[NONCE_COOKIE_NAME] = { value: nonce, secure: Rails.env.production? } + cookies.encrypted[STATE_COOKIE_NAME] = { value: state, secure: Rails.env.production?, httponly: true } + cookies.encrypted[NONCE_COOKIE_NAME] = { value: nonce, secure: Rails.env.production?, httponly: true } redirect_to uri, allow_other_host: true end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2771bf78f..659b44a31 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base def set_locale(locale) if locale && locale.to_sym.in?(I18n.available_locales) - cookies[:locale] = { value: locale, secure: Rails.env.production? } + cookies[:locale] = { value: locale, secure: Rails.env.production?, httponly: true } if user_signed_in? current_user.update(locale: locale) end diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 6c8326050..9b9fc4014 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -249,6 +249,7 @@ module Instructeurs cookies.encrypted[cookies_export_key] = { value: DateTime.current, expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT, + httponly: true, secure: Rails.env.production? } diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 96e2b62b5..a38b32cae 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,3 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, key: '_DS_session', secure: Rails.env.production? +Rails.application.config.session_store :cookie_store, key: '_DS_session', secure: Rails.env.production?, httponly: true From c50f949acd940dd0d73d464ef150606bf931636c Mon Sep 17 00:00:00 2001 From: mfo Date: Tue, 25 Jun 2024 18:01:48 +0200 Subject: [PATCH 04/39] feat(Champs::number*): normalize values by stripping spaces. --- app/models/champs/decimal_number_champ.rb | 2 +- app/models/champs/integer_number_champ.rb | 8 ++++++++ spec/models/champs/decimal_number_champ_spec.rb | 8 +++++++- spec/models/champs/integer_number_champ_spec.rb | 10 ++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb index 57091da53..dae0ce3aa 100644 --- a/app/models/champs/decimal_number_champ.rb +++ b/app/models/champs/decimal_number_champ.rb @@ -22,6 +22,6 @@ class Champs::DecimalNumberChamp < Champ def format_value return if value.blank? - self.value = value.tr(",", ".") + self.value = value.tr(",", ".").gsub(/[[:space:]]/, "") end end diff --git a/app/models/champs/integer_number_champ.rb b/app/models/champs/integer_number_champ.rb index 39adbb5fc..bd4945a16 100644 --- a/app/models/champs/integer_number_champ.rb +++ b/app/models/champs/integer_number_champ.rb @@ -1,4 +1,6 @@ class Champs::IntegerNumberChamp < Champ + before_validation :format_value + validates :value, numericality: { only_integer: true, allow_nil: true, @@ -8,4 +10,10 @@ class Champs::IntegerNumberChamp < Champ object.errors.generate_message(:value, :not_an_integer) } }, if: :validate_champ_value_or_prefill? + + def format_value + return if value.blank? + + self.value = value.gsub(/[[:space:]]/, "") + end end diff --git a/spec/models/champs/decimal_number_champ_spec.rb b/spec/models/champs/decimal_number_champ_spec.rb index 3a5674f73..57ac73cf8 100644 --- a/spec/models/champs/decimal_number_champ_spec.rb +++ b/spec/models/champs/decimal_number_champ_spec.rb @@ -24,6 +24,12 @@ describe Champs::DecimalNumberChamp do end end + context 'when value contain space' do + let(:champ) { create(:champ_decimal_number, :private, value:) } + let(:value) { ' 2.6666 ' } + it { expect(champ.value).to eq('2.6666') } + end + context 'when the value has too many decimal' do let(:value) { '2.6666' } @@ -65,7 +71,7 @@ describe Champs::DecimalNumberChamp do end context 'with number having spaces' do let(:value) { " 120 " } - it { is_expected.to be_nil } + it { is_expected.to eq(120) } end end end diff --git a/spec/models/champs/integer_number_champ_spec.rb b/spec/models/champs/integer_number_champ_spec.rb index f405b1835..7c2a4ad68 100644 --- a/spec/models/champs/integer_number_champ_spec.rb +++ b/spec/models/champs/integer_number_champ_spec.rb @@ -27,6 +27,16 @@ describe Champs::IntegerNumberChamp do end end + context 'when the value is a number with sapces' do + let(:value) { ' 120 ' } + + it 'is valid and is formated' do + is_expected.to be_truthy + champ.save! + expect(champ.value).to eq('120') + end + end + context 'when the value is blank' do let(:value) { '' } From 7c6e90df584f1e64e57604d28d42cb87797ba4df Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 4 Jul 2024 18:56:37 +0200 Subject: [PATCH 05/39] =?UTF-8?q?add=20Autorit=C3=A9=20ind=C3=A9pendante?= =?UTF-8?q?=20and=20Chambre=20parlementaire=20zones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/zones.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/zones.yml b/config/zones.yml index 6607aa601..11cd6e655 100644 --- a/config/zones.yml +++ b/config/zones.yml @@ -123,3 +123,9 @@ ministeres: labels: - '2022-05-20': "Non attribué" - '2020-07-06': "Ministère des Outre-mer" + - AI: + labels: + - '2024-07-04': "Autorité indépendante" + - CP: + labels: + - '2024-07-04': "Chambre parlementaire" From a8573febaa8b28c495a88d5d396d169e247ba120 Mon Sep 17 00:00:00 2001 From: mfo Date: Fri, 5 Jul 2024 09:34:26 +0200 Subject: [PATCH 06/39] clean(spec): remove since its now forced by a before_validation --- ...ix_decimal_number_with_spaces_task_spec.rb | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 spec/tasks/maintenance/fix_decimal_number_with_spaces_task_spec.rb diff --git a/spec/tasks/maintenance/fix_decimal_number_with_spaces_task_spec.rb b/spec/tasks/maintenance/fix_decimal_number_with_spaces_task_spec.rb deleted file mode 100644 index 1e257144f..000000000 --- a/spec/tasks/maintenance/fix_decimal_number_with_spaces_task_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -module Maintenance - RSpec.describe FixDecimalNumberWithSpacesTask do - describe "#process" do - subject(:process) { described_class.process(element) } - let(:champ) { create(:champ_decimal_number, value:) } - let(:element) { champ } - - context 'with nil' do - let(:value) { 0 } - it { expect { process }.not_to change { champ.reload.valid_value } } - end - context 'with simple number' do - let(:value) { "120" } - it { expect { process }.not_to change { champ.reload.valid_value } } - end - context 'with number having leading spaces' do - let(:value) { " 120" } - it { expect { process }.to change { champ.reload.valid_value }.from(nil).to("120") } - end - context 'with number having trailing spaces' do - let(:value) { "120 " } - it { expect { process }.to change { champ.reload.valid_value }.from(nil).to("120") } - end - context 'with number having leading and trailing spaces' do - let(:value) { " 120 " } - it { expect { process }.to change { champ.reload.valid_value }.from(nil).to("120") } - end - context 'with number having in between spaces' do - let(:value) { "1 2 0" } - it { expect { process }.to change { champ.reload.valid_value }.from(nil).to("120") } - end - context 'with number having in between tab' do - let(:value) { "\t120\t" } - it { expect { process }.to change { champ.reload.valid_value }.from(nil).to("120") } - end - end - end -end From 1e11ad4ce6b124ed77e49a81151d7c2324c43713 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:07:29 +0200 Subject: [PATCH 07/39] chore(js): update coldwired and react --- app/assets/stylesheets/01_common.scss | 4 + app/assets/stylesheets/carte.scss | 2 +- app/assets/stylesheets/forms.scss | 2 +- .../stylesheets/personnes_impliquees.scss | 2 +- app/assets/stylesheets/procedure_show.scss | 2 +- app/components/react_component.rb | 14 ++++ app/helpers/application_helper.rb | 4 - app/javascript/components/ComboMultiple.tsx | 2 +- app/javascript/components/Layout.tsx | 12 +++ .../MapEditor/components/ImportFileInput.tsx | 2 +- .../MapEditor/components/PointInput.tsx | 2 +- .../MapReader/components/GeoJSONLayer.tsx | 2 +- app/javascript/components/MapReader/index.tsx | 1 - .../components/shared/FlashMessage.tsx | 1 - .../components/shared/maplibre/MapLibre.tsx | 2 +- .../shared/maplibre/StyleControl.tsx | 2 +- .../controllers/react_controller.tsx | 72 ------------------ .../controllers/turbo_controller.ts | 41 ++++++++++ app/views/layouts/application.html.haml | 2 - .../manager/application/_javascript.html.erb | 2 + bun.lockb | Bin 501940 -> 555324 bytes package.json | 26 ++++--- tsconfig.json | 4 +- vite.config.ts | 11 ++- 24 files changed, 108 insertions(+), 106 deletions(-) create mode 100644 app/components/react_component.rb create mode 100644 app/javascript/components/Layout.tsx delete mode 100644 app/javascript/controllers/react_controller.tsx diff --git a/app/assets/stylesheets/01_common.scss b/app/assets/stylesheets/01_common.scss index 00c777a0c..47b94bc1d 100644 --- a/app/assets/stylesheets/01_common.scss +++ b/app/assets/stylesheets/01_common.scss @@ -28,3 +28,7 @@ body { .container { @extend %container; } + +react-fragment { + display: block; +} diff --git a/app/assets/stylesheets/carte.scss b/app/assets/stylesheets/carte.scss index ec8dd3c48..3de591ed2 100644 --- a/app/assets/stylesheets/carte.scss +++ b/app/assets/stylesheets/carte.scss @@ -10,7 +10,7 @@ } } -.form [data-react-component-value='MapEditor'] [data-reach-combobox-input] { +.form react-fragment[data-component-name='MapEditor'] [data-reach-combobox-input] { margin-bottom: 0; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 7b0f484cf..3b144f941 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -524,7 +524,7 @@ } } -[data-react-component-value^="ComboMultiple"] { +react-fragment[data-component-name^="ComboMultiple"] { margin-bottom: $default-fields-spacer; [data-reach-combobox-input] { diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss index d47aa2755..990876d80 100644 --- a/app/assets/stylesheets/personnes_impliquees.scss +++ b/app/assets/stylesheets/personnes_impliquees.scss @@ -9,7 +9,7 @@ margin-left: 16px; } - [data-react-component-value^="ComboMultiple"] { + react-fragment[data-component-name^="ComboMultiple"] { margin-bottom: 0; [data-reach-combobox-token-list] { diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss index 5100162cb..8b27c3436 100644 --- a/app/assets/stylesheets/procedure_show.scss +++ b/app/assets/stylesheets/procedure_show.scss @@ -45,7 +45,7 @@ display: inline-block; } - [data-react-component-value^="ComboMultiple"] { + react-fragment[data-component-name^="ComboMultiple"] { margin-bottom: $default-fields-spacer; [data-reach-combobox-token-list] { diff --git a/app/components/react_component.rb b/app/components/react_component.rb new file mode 100644 index 000000000..0f643d5ec --- /dev/null +++ b/app/components/react_component.rb @@ -0,0 +1,14 @@ +class ReactComponent < ApplicationComponent + erb_template <<-ERB + <% if content? %> + props="<%= @props.to_json %>"><%= content %> + <% else %> + props="<%= @props.to_json %>"> + <% end %> + ERB + + def initialize(name, **props) + @name = name + @props = props + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f5f859032..856cc923e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -59,10 +59,6 @@ module ApplicationHelper 'alert' end - def react_component(name, props = {}, html = {}) - tag.div(**html.merge(data: { controller: 'react', react_component_value: name, react_props_value: props.to_json })) - end - def current_email current_user&.email || current_instructeur&.email || diff --git a/app/javascript/components/ComboMultiple.tsx b/app/javascript/components/ComboMultiple.tsx index 713e64a8c..a4206b9f8 100644 --- a/app/javascript/components/ComboMultiple.tsx +++ b/app/javascript/components/ComboMultiple.tsx @@ -1,4 +1,4 @@ -import React, { +import { useMemo, useState, useRef, diff --git a/app/javascript/components/Layout.tsx b/app/javascript/components/Layout.tsx new file mode 100644 index 000000000..39a33aa9e --- /dev/null +++ b/app/javascript/components/Layout.tsx @@ -0,0 +1,12 @@ +import { I18nProvider } from 'react-aria-components'; +import { StrictMode, type ReactNode } from 'react'; + +export function Layout({ children }: { children: ReactNode }) { + const locale = document.documentElement.lang; + console.debug(`locale: ${locale}`); + return ( + + {children} + + ); +} diff --git a/app/javascript/components/MapEditor/components/ImportFileInput.tsx b/app/javascript/components/MapEditor/components/ImportFileInput.tsx index 68be216e2..28122dba5 100644 --- a/app/javascript/components/MapEditor/components/ImportFileInput.tsx +++ b/app/javascript/components/MapEditor/components/ImportFileInput.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, MouseEvent, ChangeEvent } from 'react'; +import { useState, useCallback, MouseEvent, ChangeEvent } from 'react'; import type { FeatureCollection } from 'geojson'; import invariant from 'tiny-invariant'; diff --git a/app/javascript/components/MapEditor/components/PointInput.tsx b/app/javascript/components/MapEditor/components/PointInput.tsx index 4f2fb4ead..5cd2e342f 100644 --- a/app/javascript/components/MapEditor/components/PointInput.tsx +++ b/app/javascript/components/MapEditor/components/PointInput.tsx @@ -1,4 +1,4 @@ -import React, { useState, useId } from 'react'; +import { useState, useId } from 'react'; import { fire } from '@utils'; import type { Feature, FeatureCollection } from 'geojson'; import CoordinateInput from 'react-coordinate-input'; diff --git a/app/javascript/components/MapReader/components/GeoJSONLayer.tsx b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx index add1dca03..807730cb3 100644 --- a/app/javascript/components/MapReader/components/GeoJSONLayer.tsx +++ b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { Popup, LngLatBoundsLike, LngLatLike } from 'maplibre-gl'; import type { Feature, FeatureCollection, Point } from 'geojson'; diff --git a/app/javascript/components/MapReader/index.tsx b/app/javascript/components/MapReader/index.tsx index 1f9fb07d4..a8662f152 100644 --- a/app/javascript/components/MapReader/index.tsx +++ b/app/javascript/components/MapReader/index.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import 'maplibre-gl/dist/maplibre-gl.css'; import type { FeatureCollection } from 'geojson'; diff --git a/app/javascript/components/shared/FlashMessage.tsx b/app/javascript/components/shared/FlashMessage.tsx index 4b358df3c..964a58d56 100644 --- a/app/javascript/components/shared/FlashMessage.tsx +++ b/app/javascript/components/shared/FlashMessage.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createPortal } from 'react-dom'; import invariant from 'tiny-invariant'; diff --git a/app/javascript/components/shared/maplibre/MapLibre.tsx b/app/javascript/components/shared/maplibre/MapLibre.tsx index b2045b6d0..d160775f8 100644 --- a/app/javascript/components/shared/maplibre/MapLibre.tsx +++ b/app/javascript/components/shared/maplibre/MapLibre.tsx @@ -1,4 +1,4 @@ -import React, { +import { useState, useContext, useRef, diff --git a/app/javascript/components/shared/maplibre/StyleControl.tsx b/app/javascript/components/shared/maplibre/StyleControl.tsx index ce83b75c1..afd46345c 100644 --- a/app/javascript/components/shared/maplibre/StyleControl.tsx +++ b/app/javascript/components/shared/maplibre/StyleControl.tsx @@ -1,4 +1,4 @@ -import React, { useState, useId } from 'react'; +import { useState, useId } from 'react'; import { Popover, RadioGroup } from '@headlessui/react'; import { usePopper } from 'react-popper'; import { MapIcon } from '@heroicons/react/outline'; diff --git a/app/javascript/controllers/react_controller.tsx b/app/javascript/controllers/react_controller.tsx deleted file mode 100644 index cdaff89de..000000000 --- a/app/javascript/controllers/react_controller.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; -import React, { lazy, Suspense, FunctionComponent } from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import invariant from 'tiny-invariant'; - -type Props = Record; -type Loader = () => Promise<{ default: FunctionComponent }>; -const componentsRegistry = new Map>(); -const components = import.meta.glob('../components/*.tsx'); - -for (const [path, loader] of Object.entries(components)) { - const [filename] = path.split('/').reverse(); - const componentClassName = filename.replace(/\.(ts|tsx)$/, ''); - console.debug( - `Registered lazy default export for "${componentClassName}" component` - ); - componentsRegistry.set( - componentClassName, - LoadableComponent(loader as Loader) - ); -} - -// Initialize React components when their markup appears into the DOM. -// -// Example: -//
-// -export class ReactController extends Controller { - static values = { - component: String, - props: Object - }; - - declare readonly componentValue: string; - declare readonly propsValue: Props; - - connect(): void { - this.mountComponent(this.element as HTMLElement); - } - - disconnect(): void { - unmountComponentAtNode(this.element as HTMLElement); - } - - private mountComponent(node: HTMLElement): void { - const componentName = this.componentValue; - const props = this.propsValue; - const Component = this.getComponent(componentName); - - invariant( - Component, - `Cannot find a React component with class "${componentName}"` - ); - render(, node); - } - - private getComponent(componentName: string): FunctionComponent | null { - return componentsRegistry.get(componentName) ?? null; - } -} - -const Spinner = () =>
; - -function LoadableComponent(loader: Loader): FunctionComponent { - const LazyComponent = lazy(loader); - const Component: FunctionComponent = (props: Props) => ( - }> - - - ); - return Component; -} diff --git a/app/javascript/controllers/turbo_controller.ts b/app/javascript/controllers/turbo_controller.ts index 6c8374bc5..d210b3933 100644 --- a/app/javascript/controllers/turbo_controller.ts +++ b/app/javascript/controllers/turbo_controller.ts @@ -1,7 +1,9 @@ import { Actions } from '@coldwired/actions'; import { parseTurboStream } from '@coldwired/turbo-stream'; +import { createRoot, createReactPlugin, type Root } from '@coldwired/react'; import invariant from 'tiny-invariant'; import { session as TurboSession, type StreamElement } from '@hotwired/turbo'; +import type { ComponentType } from 'react'; import { ApplicationController } from './application_controller'; @@ -20,6 +22,7 @@ export class TurboController extends ApplicationController { #submitting = false; #actions?: Actions; + #root?: Root; // `actions` instrface exposes all available actions as methods and also `applyActions` method // wich allows to apply a batch of actions. On top of regular `turbo-stream` actions we also @@ -32,6 +35,17 @@ export class TurboController extends ApplicationController { } connect() { + this.#root = createRoot({ + layoutComponentName: 'Layout/Layout', + loader, + schema: { + fragmentTagName: 'react-fragment', + componentTagName: 'react-component', + slotTagName: 'react-slot', + loadingClassName: 'loading' + } + }); + const plugin = createReactPlugin(this.#root); this.#actions = new Actions({ element: document.body, schema: { @@ -40,6 +54,7 @@ export class TurboController extends ApplicationController { focusDirectionAttribute: 'data-turbo-focus-direction', hiddenClassName: 'hidden' }, + plugins: [plugin], debug: false }); @@ -73,6 +88,11 @@ export class TurboController extends ApplicationController { }); } + disconnect(): void { + this.#actions?.disconnect(); + this.#root?.destroy(); + } + private startSpinner() { this.#submitting = true; this.actions.show({ targets: this.spinnerTargets }); @@ -89,3 +109,24 @@ export class TurboController extends ApplicationController { } } } + +type Loader = (exportName: string) => Promise>; +const componentsRegistry: Record = {}; +const components = import.meta.glob('../components/*.tsx'); + +const loader: Loader = (name) => { + const [moduleName, exportName] = name.split('/'); + const loader = componentsRegistry[moduleName]; + invariant(loader, `Cannot find a React component with name "${name}"`); + return loader(exportName ?? 'default'); +}; + +for (const [path, loader] of Object.entries(components)) { + const [filename] = path.split('/').reverse(); + const componentClassName = filename.replace(/\.(ts|tsx)$/, ''); + console.debug(`Registered lazy export for "${componentClassName}" component`); + componentsRegistry[componentClassName] = (exportName) => + loader().then( + (m) => (m as Record>)[exportName] + ); +} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index ceb84ff47..91fc4699e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -59,7 +59,5 @@ - else = render 'footer' - - if Rails.env.development? - = vite_typescript_tag 'axe-core' = yield :charts_js = render Attachment::ProgressBarComponent.new diff --git a/app/views/manager/application/_javascript.html.erb b/app/views/manager/application/_javascript.html.erb index 570441489..56f773546 100644 --- a/app/views/manager/application/_javascript.html.erb +++ b/app/views/manager/application/_javascript.html.erb @@ -11,6 +11,8 @@ by providing a `content_for(:javascript)` block. <%= javascript_include_tag js_path %> <% end %> +<%= vite_client_tag %> +<%= vite_react_refresh_tag %> <%= vite_typescript_tag 'manager' %> <%= yield :javascript %> diff --git a/bun.lockb b/bun.lockb index 7c777a1117455aa446b2e741a83ac6e465d43f38..608dba50f0f296e4f8b60dd549f48a635868a277 100755 GIT binary patch delta 148202 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 aSSj01LfaKv9Pa)rRmHuicz%oHzyAj^Aa3*k delta 108745 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 Date: Mon, 6 May 2024 18:08:26 +0200 Subject: [PATCH 08/39] feat(js): implement react aria combobox --- app/assets/stylesheets/dsfr.scss | 83 +++- app/assets/stylesheets/forms.scss | 4 - app/components/dsfr/input_errorable.rb | 16 +- app/javascript/components/ComboBox.tsx | 326 +++++++++++++ app/javascript/components/react-aria/hooks.ts | 438 ++++++++++++++++++ app/javascript/components/react-aria/props.ts | 73 +++ .../controllers/menu_button_controller.ts | 1 + 7 files changed, 914 insertions(+), 27 deletions(-) create mode 100644 app/javascript/components/ComboBox.tsx create mode 100644 app/javascript/components/react-aria/hooks.ts create mode 100644 app/javascript/components/react-aria/props.ts diff --git a/app/assets/stylesheets/dsfr.scss b/app/assets/stylesheets/dsfr.scss index 2b312fc36..4de7cf6e9 100644 --- a/app/assets/stylesheets/dsfr.scss +++ b/app/assets/stylesheets/dsfr.scss @@ -32,29 +32,78 @@ trix-editor.fr-input { } .fr-ds-combobox { - .fr-menu { - width: 100%; - - .fr-menu__list { - width: 100%; - max-height: 300px; - } - } - .fr-autocomplete { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z'%3E%3C/path%3E%3C/svg%3E"); } } +.fr-ds-combobox__menu { + &[data-placement=top] { + --origin: translateY(8px); + } + + &[data-placement=bottom] { + --origin: translateY(-8px); + } + + &[data-placement=right] { + --origin: translateX(-8px); + } + + &[data-placement=left] { + --origin: translateX(8px); + } + + &[data-entering] { + animation: popover-slide 200ms; + } + + &.fr-menu { + width: var(--trigger-width); + top: unset; + + .fr-menu__list { + display: block; + width: unset; + max-height: 300px; + overflow: auto; + } + + .fr-menu__item { + &[data-selected] { + font-weight: bold; + } + + &[data-focused] { + font-weight: bold; + } + } + } +} + +@keyframes popover-slide { + from { + transform: var(--origin); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + @media (max-width: 62em) { - .fr-ds-combobox .fr-menu .fr-menu__list { - z-index: calc(var(--ground) + 1000); - background-color: var(--background-default-grey); - --idle: transparent; - --hover: var(--background-overlap-grey-hover); - --active: var(--background-overlap-grey-active); - filter: drop-shadow(var(--overlap-shadow)); - box-shadow: inset 0 1px 0 0 var(--border-open-blue-france); + .fr-ds-combobox__menu { + &.fr-menu .fr-menu__list { + z-index: calc(var(--ground) + 1000); + background-color: var(--background-default-grey); + --idle: transparent; + --hover: var(--background-overlap-grey-hover); + --active: var(--background-overlap-grey-active); + filter: drop-shadow(var(--overlap-shadow)); + box-shadow: inset 0 1px 0 0 var(--border-open-blue-france); + } } } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 3b144f941..c743405fd 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -634,10 +634,6 @@ textarea::placeholder { .fr-menu__item { list-style-type: none; margin-bottom: $default-spacer; - - &[aria-selected] { - font-weight: bold; - } } } diff --git a/app/components/dsfr/input_errorable.rb b/app/components/dsfr/input_errorable.rb index efe7de775..9ae359fd8 100644 --- a/app/components/dsfr/input_errorable.rb +++ b/app/components/dsfr/input_errorable.rb @@ -73,22 +73,26 @@ module Dsfr } end - def input_opts(other_opts = {}) + def react_input_opts(other_opts = {}) + input_opts(other_opts, true) + end + + def input_opts(other_opts = {}, react = false) @opts = @opts.deep_merge!(other_opts) - @opts[:class] = class_names(map_array_to_hash_with_true(@opts[:class]) + @opts[react ? :class_name : :class] = class_names(map_array_to_hash_with_true(@opts[:class]) .merge({ 'fr-password__input': password?, - 'fr-input': true, + 'fr-input': !react, 'fr-mb-0': true }.merge(input_error_class_names))) if errors_on_attribute? - @opts.deep_merge!(aria: { describedby: describedby_id }) + @opts.deep_merge!('aria-describedby': describedby_id) elsif hintable? - @opts.deep_merge!(aria: { describedby: hint_id }) + @opts.deep_merge!('aria-describedby': hint_id) end if @required - @opts[:required] = true + @opts[react ? :is_required : :required] = true end if email? diff --git a/app/javascript/components/ComboBox.tsx b/app/javascript/components/ComboBox.tsx new file mode 100644 index 000000000..63abf1a88 --- /dev/null +++ b/app/javascript/components/ComboBox.tsx @@ -0,0 +1,326 @@ +import type { ListBoxItemProps } from 'react-aria-components'; +import { + ComboBox as AriaComboBox, + ListBox, + ListBoxItem, + Popover, + Input, + Label, + Text, + Button, + TagGroup, + TagList, + Tag +} from 'react-aria-components'; +import { useMemo, useRef, createContext, useContext } from 'react'; +import type { RefObject } from 'react'; +import { findOrCreateContainerElement } from '@coldwired/react'; + +import { + useLabelledBy, + useDispatchChangeEvent, + useMultiList, + useSingleList, + useRemoteList, + createLoader, + type ComboBoxProps +} from './react-aria/hooks'; +import { + type Item, + SingleComboBoxProps, + MultiComboBoxProps, + RemoteComboBoxProps +} from './react-aria/props'; + +const getPortal = () => findOrCreateContainerElement('rac-portal'); + +export function ComboBox({ + children, + label, + description, + className, + inputRef, + ...props +}: ComboBoxProps & { inputRef?: RefObject }) { + return ( + + {label ? : null} + {description ? ( + + {description} + + ) : null} +
+ + +
+ + {children} + +
+ ); +} + +export function ComboBoxItem(props: ListBoxItemProps) { + return ; +} + +export function SingleComboBox({ + children, + ...maybeProps +}: SingleComboBoxProps) { + const { + 'aria-labelledby': ariaLabelledby, + items: defaultItems, + selectedKey: defaultSelectedKey, + emptyFilterKey, + name, + formValue, + form, + data, + ...props + } = useMemo(() => SingleComboBoxProps.parse(maybeProps), [maybeProps]); + + const labelledby = useLabelledBy(props.id, ariaLabelledby); + const { ref, dispatch } = useDispatchChangeEvent(); + + const { selectedItem, ...comboBoxProps } = useSingleList({ + defaultItems, + defaultSelectedKey, + emptyFilterKey, + onChange: dispatch + }); + + return ( + <> + + {(item) => {item.label}} + + {children || name ? ( + + + {name ? ( + + ) : null} + {children} + + + ) : null} + + ); +} + +export function MultiComboBox(maybeProps: MultiComboBoxProps) { + const { + 'aria-labelledby': ariaLabelledby, + items: defaultItems, + selectedKeys: defaultSelectedKeys, + name, + form, + formValue, + allowsCustomValue, + valueSeparator, + ...props + } = useMemo(() => MultiComboBoxProps.parse(maybeProps), [maybeProps]); + + const labelledby = useLabelledBy(props.id, ariaLabelledby); + const { ref, dispatch } = useDispatchChangeEvent(); + const inputRef = useRef(null); + + const { selectedItems, hiddenInputValues, onRemove, ...comboBoxProps } = + useMultiList({ + defaultItems, + defaultSelectedKeys, + onChange: dispatch, + formValue, + allowsCustomValue, + valueSeparator, + focusInput: () => { + inputRef.current?.focus(); + } + }); + + return ( +
+ {selectedItems.length > 0 ? ( + + + {selectedItems.map((item) => ( + + {item.label} + + + ))} + + + ) : null} + + {(item) => {item.label}} + + {name ? ( + + {hiddenInputValues.map((value) => ( + + ))} + + ) : null} +
+ ); +} + +export function RemoteComboBox({ + loader, + onChange, + children, + ...maybeProps +}: RemoteComboBoxProps) { + const { + 'aria-labelledby': ariaLabelledby, + items: defaultItems, + selectedKey: defaultSelectedKey, + allowsCustomValue, + minimumInputLength, + limit, + formValue, + name, + form, + data, + ...props + } = useMemo(() => RemoteComboBoxProps.parse(maybeProps), [maybeProps]); + + const labelledby = useLabelledBy(props.id, ariaLabelledby); + const { ref, dispatch } = useDispatchChangeEvent(); + + const load = useMemo( + () => + typeof loader == 'string' + ? createLoader(loader, { minimumInputLength, limit }) + : loader, + [loader, minimumInputLength, limit] + ); + const { selectedItem, ...comboBoxProps } = useRemoteList({ + allowsCustomValue, + defaultItems, + defaultSelectedKey, + load, + onChange: (item) => { + onChange?.(item); + dispatch(); + } + }); + + return ( + <> + 0} + allowsCustomValue={allowsCustomValue} + aria-labelledby={labelledby} + {...comboBoxProps} + {...props} + > + {(item) => {item.label}} + + {children || name ? ( + + + {name ? ( + + ) : null} + {children} + + + ) : null} + + ); +} + +export function ComboBoxValueSlot({ + field, + name, + form, + data +}: { + field: 'label' | 'value' | 'data'; + name: string; + form?: string; + data?: Record; +}) { + const selectedItem = useContext(SelectedItemContext); + const value = getSelectedValue(selectedItem, field); + const dataProps = Object.fromEntries( + Object.entries(data ?? {}).map(([key, value]) => [ + `data-${key.replace(/_/g, '-')}`, + value + ]) + ); + return ( + + ); +} + +const SelectedItemContext = createContext(null); +const SelectedItemProvider = SelectedItemContext.Provider; + +function getSelectedValue( + selectedItem: Item | null, + field: 'label' | 'value' | 'data' +): string { + if (selectedItem == null) { + return ''; + } else if (field == 'data') { + if (typeof selectedItem.data == 'string') { + return selectedItem.data; + } else if (!selectedItem.data) { + return ''; + } + return JSON.stringify(selectedItem.data); + } + return selectedItem[field]; +} diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts new file mode 100644 index 000000000..7974e304e --- /dev/null +++ b/app/javascript/components/react-aria/hooks.ts @@ -0,0 +1,438 @@ +import type { + ComboBoxProps as AriaComboBoxProps, + TagGroupProps +} from 'react-aria-components'; +import { useAsyncList, type AsyncListOptions } from 'react-stately'; +import { useMemo, useRef, useState, useEffect } from 'react'; +import type { Key } from 'react'; +import { matchSorter } from 'match-sorter'; +import { useDebounceCallback } from 'usehooks-ts'; +import { useEvent } from 'react-use-event-hook'; +import isEqual from 'react-fast-compare'; + +import { Item } from './props'; + +export type Loader = AsyncListOptions['load']; + +export interface ComboBoxProps + extends Omit, 'children'> { + children: React.ReactNode | ((item: Item) => React.ReactNode); + label?: string; + description?: string; +} + +const inputMap = new WeakMap(); +export function useDispatchChangeEvent() { + const ref = useRef(null); + + return { + ref, + dispatch: () => { + requestAnimationFrame(() => { + const input = ref.current?.querySelector('input'); + if (input) { + const value = input.value; + const prevValue = inputMap.get(input) || ''; + if (value != prevValue) { + inputMap.set(input, value); + input.dispatchEvent(new Event('change', { bubbles: true })); + } + } + }); + } + }; +} + +export function useSingleList({ + defaultItems, + defaultSelectedKey, + emptyFilterKey, + onChange +}: { + defaultItems?: Item[]; + defaultSelectedKey?: string | null; + emptyFilterKey?: string; + onChange?: (item: Item | null) => void; +}) { + const [selectedKey, setSelectedKey] = useState(defaultSelectedKey); + const items = useMemo(() => defaultItems || [], [defaultItems]); + const selectedItem = useMemo( + () => items.find((item) => item.value == selectedKey) ?? null, + [items, selectedKey] + ); + const [inputValue, setInputValue] = useState(() => selectedItem?.label ?? ''); + // show fallback item when input value is not matching any items + const fallbackItem = useMemo( + () => items.find((item) => item.value == emptyFilterKey), + [items, emptyFilterKey] + ); + const filteredItems = useMemo(() => { + if (inputValue == '') { + return items; + } + const filteredItems = matchSorter(items, inputValue, { keys: ['label'] }); + if (filteredItems.length == 0 && fallbackItem) { + return [fallbackItem]; + } else { + return filteredItems; + } + }, [items, inputValue, fallbackItem]); + + const initialSelectedKeyRef = useRef(defaultSelectedKey); + + const setSelection = useEvent((key?: string | null) => { + const inputValue = defaultSelectedKey + ? items.find((item) => item.value == defaultSelectedKey)?.label + : ''; + setSelectedKey(key); + setInputValue(inputValue ?? ''); + }); + const onSelectionChange = useEvent< + NonNullable + >((key) => { + setSelection(key ? String(key) : null); + const item = + typeof key != 'string' + ? null + : selectedItem?.value == key + ? selectedItem + : items.find((item) => item.value == key) ?? null; + onChange?.(item); + }); + const onInputChange = useEvent>( + (value) => { + setInputValue(value); + if (value == '') { + onSelectionChange(null); + } + } + ); + + // reset default selected key when props change + useEffect(() => { + if (initialSelectedKeyRef.current != defaultSelectedKey) { + initialSelectedKeyRef.current = defaultSelectedKey; + setSelection(defaultSelectedKey); + } + }, [defaultSelectedKey, setSelection]); + + return { + selectedItem, + selectedKey, + onSelectionChange, + inputValue, + onInputChange, + items: filteredItems + }; +} + +export function useMultiList({ + defaultItems, + defaultSelectedKeys, + allowsCustomValue, + valueSeparator, + onChange, + focusInput, + formValue +}: { + defaultItems?: Item[]; + defaultSelectedKeys?: string[]; + allowsCustomValue?: boolean; + valueSeparator?: string; + onChange?: () => void; + focusInput?: () => void; + formValue?: 'text' | 'key'; +}) { + const valueSeparatorRegExp = useMemo( + () => (valueSeparator ? new RegExp(valueSeparator) : /\s|,|;/), + [valueSeparator] + ); + const [selectedKeys, setSelectedKeys] = useState( + () => new Set(defaultSelectedKeys ?? []) + ); + const [inputValue, setInputValue] = useState(''); + const items = useMemo(() => defaultItems || [], [defaultItems]); + const itemsIndex = useMemo(() => { + const index = new Map(); + for (const item of items) { + index.set(item.value, item); + } + return index; + }, [items]); + const filteredItems = useMemo( + () => + inputValue.length == 0 + ? items + : matchSorter(items, inputValue, { keys: ['label'] }), + [items, inputValue] + ); + const selectedItems = useMemo(() => { + const selectedItems: Item[] = []; + for (const key of selectedKeys) { + const item = itemsIndex.get(key); + if (item) { + selectedItems.push(item); + } else if (allowsCustomValue) { + selectedItems.push({ label: key, value: key }); + } + } + return selectedItems; + }, [itemsIndex, selectedKeys, allowsCustomValue]); + const hiddenInputValues = useMemo(() => { + const values = selectedItems.map((item) => + formValue == 'text' || allowsCustomValue ? item.label : item.value + ); + if (!allowsCustomValue || inputValue == '') { + return values; + } + return [ + ...new Set([ + ...values, + ...inputValue.split(valueSeparatorRegExp).filter(Boolean) + ]) + ]; + }, [ + selectedItems, + inputValue, + valueSeparatorRegExp, + allowsCustomValue, + formValue + ]); + const isSelectionSetRef = useRef(false); + const initialSelectedKeysRef = useRef(defaultSelectedKeys); + + // reset default selected keys when props change + useEffect(() => { + if (!isEqual(initialSelectedKeysRef.current, defaultSelectedKeys)) { + initialSelectedKeysRef.current = defaultSelectedKeys; + setSelectedKeys(new Set(defaultSelectedKeys)); + } + }, [defaultSelectedKeys]); + + const onSelectionChange = useEvent< + NonNullable + >((key) => { + if (key) { + isSelectionSetRef.current = true; + setSelectedKeys((keys) => { + const selectedKeys = new Set(keys.values()); + selectedKeys.add(String(key)); + return selectedKeys; + }); + setInputValue(''); + onChange?.(); + } + }); + + const onInputChange = useEvent>( + (value) => { + const isSelectionSet = isSelectionSetRef.current; + isSelectionSetRef.current = false; + if (isSelectionSet) { + setInputValue(''); + return; + } + if (allowsCustomValue) { + const values = value.split(valueSeparatorRegExp); + // if input contains a separator, add all values + if (values.length > 1) { + const addedKeys = values.filter(Boolean); + setSelectedKeys((keys) => { + const selectedKeys = new Set(keys.values()); + for (const key of addedKeys) { + selectedKeys.add(key); + } + return selectedKeys; + }); + setInputValue(''); + } else { + setInputValue(value); + } + onChange?.(); + } else { + setInputValue(value); + } + } + ); + + const onRemove = useEvent>( + (removedKeys) => { + setSelectedKeys((keys) => { + const selectedKeys = new Set(keys.values()); + for (const key of removedKeys) { + selectedKeys.delete(String(key)); + } + // focus input when all items are removed + if (selectedKeys.size == 0) { + focusInput?.(); + } + return selectedKeys; + }); + onChange?.(); + } + ); + + return { + onRemove, + onSelectionChange, + onInputChange, + selectedItems, + items: filteredItems, + hiddenInputValues, + inputValue + }; +} + +export function useRemoteList({ + load, + defaultItems, + defaultSelectedKey, + onChange, + debounce, + allowsCustomValue +}: { + load: Loader; + defaultItems?: Item[]; + defaultSelectedKey?: Key | null; + onChange?: (item: Item | null) => void; + debounce?: number; + allowsCustomValue?: boolean; +}) { + const [defaultSelectedItem, setSelectedItem] = useState(() => { + if (defaultItems) { + return ( + defaultItems.find((item) => item.value == defaultSelectedKey) ?? null + ); + } + return null; + }); + const [inputValue, setInputValue] = useState( + defaultSelectedItem?.label ?? '' + ); + const selectedItem = useMemo(() => { + if (defaultSelectedItem) { + return defaultSelectedItem; + } + if (allowsCustomValue && inputValue != '') { + return { label: inputValue, value: inputValue }; + } + return null; + }, [defaultSelectedItem, inputValue, allowsCustomValue]); + const list = useAsyncList({ getKey, load }); + const setFilterText = useEvent((filterText: string) => { + list.setFilterText(filterText); + }); + const debouncedSetFilterText = useDebounceCallback( + setFilterText, + debounce ?? 300 + ); + + const onSelectionChange = useEvent< + NonNullable + >((key) => { + const item = + typeof key != 'string' + ? null + : selectedItem?.value == key + ? selectedItem + : list.getItem(key); + setSelectedItem(item); + if (item) { + setInputValue(item.label); + } else if (!allowsCustomValue) { + setInputValue(''); + } + onChange?.(item); + }); + + const onInputChange = useEvent>( + (value) => { + debouncedSetFilterText(value); + setInputValue(value); + if (value == '') { + onSelectionChange(null); + } else if (allowsCustomValue && selectedItem?.label != value) { + onChange?.(selectedItem); + } + } + ); + + // add to items list current selected item if it's not in the list + const items = + selectedItem && !list.getItem(selectedItem.value) + ? [selectedItem, ...list.items] + : list.items; + + return { + selectedItem, + selectedKey: selectedItem?.value ?? null, + onSelectionChange, + inputValue, + onInputChange, + items + }; +} + +function getKey(item: Item) { + return item.value; +} + +export const createLoader: ( + source: string, + options?: { + minimumInputLength?: number; + limit?: number; + param?: string; + } +) => Loader = + (source, options) => + async ({ signal, filterText }) => { + const url = new URL(source, location.href); + const minimumInputLength = options?.minimumInputLength ?? 2; + const param = options?.param ?? 'q'; + const limit = options?.limit ?? 10; + + if (!filterText || filterText.length < minimumInputLength) { + return { items: [] }; + } + url.searchParams.set(param, filterText); + try { + const response = await fetch(url.toString(), { + headers: { accept: 'application/json' }, + signal + }); + if (response.ok) { + const json = await response.json(); + const result = Item.array().safeParse(json); + if (result.success) { + const items = matchSorter(result.data, filterText, { + keys: ['label'] + }); + return { + items: limit ? items.slice(0, limit) : items + }; + } + } + return { items: [] }; + } catch { + return { items: [] }; + } + }; + +export function useLabelledBy(id?: string, ariaLabelledby?: string) { + return useMemo( + () => (ariaLabelledby ? ariaLabelledby : findLabelledbyId(id)), + [id, ariaLabelledby] + ); +} + +function findLabelledbyId(id?: string) { + if (!id) { + return; + } + const label = document.querySelector(`[for="${id}"]`); + if (!label?.id) { + return; + } + return label.id; +} diff --git a/app/javascript/components/react-aria/props.ts b/app/javascript/components/react-aria/props.ts new file mode 100644 index 000000000..e67ac1096 --- /dev/null +++ b/app/javascript/components/react-aria/props.ts @@ -0,0 +1,73 @@ +import type { ReactNode } from 'react'; +import { z } from 'zod'; + +import type { Loader } from './hooks'; + +export const Item = z.object({ + label: z.string(), + value: z.string(), + data: z.any().optional() +}); +export type Item = z.infer; + +const ComboBoxPropsSchema = z + .object({ + id: z.string(), + className: z.string(), + name: z.string(), + label: z.string(), + description: z.string(), + isRequired: z.boolean(), + 'aria-label': z.string(), + 'aria-labelledby': z.string(), + 'aria-describedby': z.string(), + items: z + .array(Item) + .or( + z + .string() + .array() + .transform((items) => + items.map((label) => ({ label, value: label })) + ) + ) + .or( + z + .tuple([z.string(), z.string().or(z.number())]) + .array() + .transform((items) => + items.map(([label, value]) => ({ + label, + value: String(value) + })) + ) + ), + formValue: z.enum(['text', 'key']), + form: z.string(), + data: z.record(z.string()) + }) + .partial(); +export const SingleComboBoxProps = ComboBoxPropsSchema.extend({ + selectedKey: z.string().nullable(), + emptyFilterKey: z.string() +}).partial(); +export const MultiComboBoxProps = ComboBoxPropsSchema.extend({ + selectedKeys: z.string().array(), + allowsCustomValue: z.boolean(), + valueSeparator: z.string() +}).partial(); +export const RemoteComboBoxProps = ComboBoxPropsSchema.extend({ + selectedKey: z.string().nullable(), + minimumInputLength: z.number(), + limit: z.number(), + allowsCustomValue: z.boolean() +}).partial(); +export type SingleComboBoxProps = z.infer & { + children?: ReactNode; +}; +export type MultiComboBoxProps = z.infer; +export type RemoteComboBoxProps = z.infer & { + children?: ReactNode; + loader: Loader | string; + onChange?: (item: Item | null) => void; +}; diff --git a/app/javascript/controllers/menu_button_controller.ts b/app/javascript/controllers/menu_button_controller.ts index c0b407242..a5edf9b53 100644 --- a/app/javascript/controllers/menu_button_controller.ts +++ b/app/javascript/controllers/menu_button_controller.ts @@ -86,6 +86,7 @@ export class MenuButtonController extends ApplicationController { target.isConnected && !this.element.contains(target) && !target.closest('reach-portal') && + !target.closest('#rac-portal') && this.isOpen ); } From 79a65a48476c8bbf8c42d76b95bb6e390acdb2b3 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:09:10 +0200 Subject: [PATCH 09/39] refactor(champ): update champ address --- app/components/editable_champ/address_component.rb | 11 +++++++++++ .../address_component/address_component.html.haml | 6 +++--- app/models/champs/address_champ.rb | 12 ++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/components/editable_champ/address_component.rb b/app/components/editable_champ/address_component.rb index 0bcefb5e0..f0c7a8d75 100644 --- a/app/components/editable_champ/address_component.rb +++ b/app/components/editable_champ/address_component.rb @@ -2,4 +2,15 @@ class EditableChamp::AddressComponent < EditableChamp::EditableChampBaseComponen def dsfr_input_classname 'fr-select' end + + def react_props + react_input_opts(id: @champ.input_id, + class: 'fr-mt-1w', + name: @form.field_name(:value), + selected_key: @champ.value, + items: @champ.selected_items, + loader: data_sources_data_source_adresse_path, + minimum_input_length: 2, + allows_custom_value: true) + end end diff --git a/app/components/editable_champ/address_component/address_component.html.haml b/app/components/editable_champ/address_component/address_component.html.haml index e1029f05a..6df764db4 100644 --- a/app/components/editable_champ/address_component/address_component.html.haml +++ b/app/components/editable_champ/address_component/address_component.html.haml @@ -1,3 +1,3 @@ -= render Dsfr::ComboboxComponent.new form: @form, url: data_sources_data_source_adresse_path, selected: @champ.value, allows_custom_value: true, input_html_options: { name: :value, id: @champ.input_id, class: 'fr-select', describedby: @champ.describedby_id } do - = @form.hidden_field :external_id, data: { value_slot: 'value' } - = @form.hidden_field :feature, data: { value_slot: 'data' } +%react-fragment + = render ReactComponent.new "ComboBox/RemoteComboBox", **react_props do + = render ReactComponent.new "ComboBox/ComboBoxValueSlot", field: :data, name: @form.field_name(:feature) diff --git a/app/models/champs/address_champ.rb b/app/models/champs/address_champ.rb index 1c4d9e78f..c07ced241 100644 --- a/app/models/champs/address_champ.rb +++ b/app/models/champs/address_champ.rb @@ -3,10 +3,6 @@ class Champs::AddressChamp < Champs::TextChamp data.present? end - def feature - data.to_json if full_address? - end - def feature=(value) if value.blank? self.data = nil @@ -22,6 +18,14 @@ class Champs::AddressChamp < Champs::TextChamp self.data = nil end + def selected_items + if value.present? + [{ value:, label: value, data: full_address? ? data : nil }] + else + [] + end + end + def address full_address? ? data : nil end From f1f1af4e619a87e1ee221ca50743245f39ba5c22 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:09:44 +0200 Subject: [PATCH 10/39] refactor(champ): update champ annuaire education --- .../annuaire_education_component.rb | 17 +++++---- .../annuaire_education_component.html.haml | 10 ++--- .../data_sources/education_controller.rb | 38 +++++++++++++++++++ app/models/champs/annuaire_education_champ.rb | 11 ++---- app/models/type_de_champ.rb | 3 +- config/routes.rb | 1 + 6 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 app/controllers/data_sources/education_controller.rb diff --git a/app/components/editable_champ/annuaire_education_component.rb b/app/components/editable_champ/annuaire_education_component.rb index 847b6cc21..fc38867bb 100644 --- a/app/components/editable_champ/annuaire_education_component.rb +++ b/app/components/editable_champ/annuaire_education_component.rb @@ -1,12 +1,15 @@ -class EditableChamp::AnnuaireEducationComponent < EditableChamp::ComboSearchComponent +class EditableChamp::AnnuaireEducationComponent < EditableChamp::EditableChampBaseComponent def dsfr_input_classname - 'fr-input' + 'fr-select' end - def react_input_opts - opts = input_opts(id: @champ.input_id, required: @champ.required?, aria: { describedby: @champ.describedby_id }) - opts[:className] = "#{opts.delete(:class)} fr-mt-1w" - - opts + def react_props + react_input_opts(id: @champ.input_id, + class: "fr-mt-1w", + name: @form.field_name(:external_id), + selected_key: @champ.external_id, + items: @champ.selected_items, + loader: data_sources_data_source_education_path, + minimum_input_length: 3) end end diff --git a/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml b/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml index 00ce7bbba..a3a489e4d 100644 --- a/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml +++ b/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml @@ -1,7 +1,3 @@ -- render_parent - -= @form.hidden_field :value -= @form.hidden_field :external_id -= react_component("ComboAnnuaireEducationSearch", - **react_input_opts, - **react_combo_props) +%react-fragment + = render ReactComponent.new "ComboBox/RemoteComboBox", **react_props do + = render ReactComponent.new "ComboBox/ComboBoxValueSlot", field: :label, name: @form.field_name(:value) diff --git a/app/controllers/data_sources/education_controller.rb b/app/controllers/data_sources/education_controller.rb new file mode 100644 index 000000000..99722ef33 --- /dev/null +++ b/app/controllers/data_sources/education_controller.rb @@ -0,0 +1,38 @@ +class DataSources::EducationController < ApplicationController + def search + if params[:q].present? && params[:q].length >= 3 + response = fetch_results + + if response.success? + results = JSON.parse(response.body, symbolize_names: true) + + return render json: format_results(results) + end + end + + render json: [] + + rescue JSON::ParserError => e + Sentry.set_extras(body: response.body, code: response.code) + Sentry.capture_exception(e) + render json: [] + end + + private + + def fetch_results + Typhoeus.get("#{API_EDUCATION_URL}/search", params: { q: params[:q], rows: 5, dataset: 'fr-en-annuaire-education' }, timeout: 3) + end + + def format_results(results) + results[:records].map do |record| + fields = record.fetch(:fields) + value = fields.fetch(:identifiant_de_l_etablissement) + { + label: "#{fields.fetch(:nom_etablissement)}, #{fields.fetch(:nom_commune)} (#{value})", + value:, + data: record + } + end + end +end diff --git a/app/models/champs/annuaire_education_champ.rb b/app/models/champs/annuaire_education_champ.rb index e691b707b..a36437902 100644 --- a/app/models/champs/annuaire_education_champ.rb +++ b/app/models/champs/annuaire_education_champ.rb @@ -7,14 +7,11 @@ class Champs::AnnuaireEducationChamp < Champs::TextChamp APIEducation::AnnuaireEducationAdapter.new(external_id).to_params end - def update_with_external_data!(data:) - if data&.is_a?(Hash) && data['nom_etablissement'].present? && data['nom_commune'].present? && data['identifiant_de_l_etablissement'].present? - update!( - data: data, - value: "#{data['nom_etablissement']}, #{data['nom_commune']} (#{data['identifiant_de_l_etablissement']})" - ) + def selected_items + if external_id.present? + [{ value: external_id, label: value }] else - update!(data: data) + [] end end end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 5ee6bc037..abc959a7b 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -641,8 +641,7 @@ class TypeDeChamp < ApplicationRecord # We should refresh all champs after update except for champs using react or custom refresh # logic (RNA, SIRET, etc.) case type_champ - when type_champs.fetch(:annuaire_education), - type_champs.fetch(:carte), + when type_champs.fetch(:carte), type_champs.fetch(:piece_justificative), type_champs.fetch(:titre_identite), type_champs.fetch(:rna), diff --git a/config/routes.rb b/config/routes.rb index 06f613da0..2c2c86ed0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -251,6 +251,7 @@ Rails.application.routes.draw do namespace :data_sources do get :adresse, to: 'adresse#search', as: :data_source_adresse get :commune, to: 'commune#search', as: :data_source_commune + get :education, to: 'education#search', as: :data_source_education get :search_domaine_fonct, to: 'chorus#search_domaine_fonct', as: :search_domaine_fonct get :search_centre_couts, to: 'chorus#search_centre_couts', as: :search_centre_couts From 2f2edfdfc77a2d47dcd75ec26799615357a4ee61 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:10:30 +0200 Subject: [PATCH 11/39] refactor(champ): update champ carte --- .../editable_champ/carte_component.rb | 12 +++-- .../carte_component/carte_component.html.haml | 12 +---- .../MapEditor/components/AddressInput.tsx | 48 +++++++++---------- app/javascript/components/MapEditor/index.tsx | 16 ++----- app/views/shared/champs/carte/_show.html.haml | 3 +- 5 files changed, 40 insertions(+), 51 deletions(-) diff --git a/app/components/editable_champ/carte_component.rb b/app/components/editable_champ/carte_component.rb index 189fcb76a..b7ecb9a8c 100644 --- a/app/components/editable_champ/carte_component.rb +++ b/app/components/editable_champ/carte_component.rb @@ -4,10 +4,14 @@ class EditableChamp::CarteComponent < EditableChamp::EditableChampBaseComponent :fieldset end - def initialize(**args) - super(**args) - - @autocomplete_component = EditableChamp::ComboSearchComponent.new(**args) + def react_props + { + feature_collection: @champ.to_feature_collection, + champ_id: @champ.input_id, + url: update_path, + adresse_source: data_sources_data_source_adresse_path, + options: @champ.render_options + } end def update_path diff --git a/app/components/editable_champ/carte_component/carte_component.html.haml b/app/components/editable_champ/carte_component/carte_component.html.haml index db14600f9..052e979a9 100644 --- a/app/components/editable_champ/carte_component/carte_component.html.haml +++ b/app/components/editable_champ/carte_component/carte_component.html.haml @@ -1,14 +1,6 @@ .fr-fieldset__element - = render @autocomplete_component - - = react_component("MapEditor", - { featureCollection: @champ.to_feature_collection, - champId: @champ.input_id, - url: update_path, - options: @champ.render_options, - autocompleteAnnounceTemplateId: @autocomplete_component.announce_template_id, - autocompleteScreenReaderInstructions: t("combo_search_component.screen_reader_instructions") }, - {class: 'width-100'}) + %react-fragment.width-100 + = render ReactComponent.new "MapEditor", **react_props .geo-areas{ id: dom_id(@champ, :geo_areas) } = render Dossiers::GeoAreasComponent.new(champ: @champ, editing: true) diff --git a/app/javascript/components/MapEditor/components/AddressInput.tsx b/app/javascript/components/MapEditor/components/AddressInput.tsx index 0e2d2d2a5..0e7c22a22 100644 --- a/app/javascript/components/MapEditor/components/AddressInput.tsx +++ b/app/javascript/components/MapEditor/components/AddressInput.tsx @@ -1,33 +1,33 @@ -import React from 'react'; import { fire } from '@utils'; import type { FeatureCollection } from 'geojson'; -import ComboAdresseSearch from '../../ComboAdresseSearch'; -import { ComboSearchProps } from '~/components/ComboSearch'; +import { RemoteComboBox } from '../../ComboBox'; -export function AddressInput( - comboProps: Pick< - ComboSearchProps, - 'screenReaderInstructions' | 'announceTemplateId' - > & { featureCollection: FeatureCollection; champId: string } -) { +export function AddressInput({ + source, + featureCollection, + champId +}: { + source: string; + featureCollection: FeatureCollection; + champId: string; +}) { return ( -
- { - fire(document, 'map:zoom', { - featureCollection: comboProps.featureCollection, - feature - }); +
+ { + if (item && item.data) { + fire(document, 'map:zoom', { + featureCollection, + feature: item.data + }); + } }} - {...comboProps} />
); diff --git a/app/javascript/components/MapEditor/index.tsx b/app/javascript/components/MapEditor/index.tsx index 0a918c981..5bea4796c 100644 --- a/app/javascript/components/MapEditor/index.tsx +++ b/app/javascript/components/MapEditor/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { CursorClickIcon } from '@heroicons/react/outline'; import 'maplibre-gl/dist/maplibre-gl.css'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; @@ -12,21 +12,18 @@ import { AddressInput } from './components/AddressInput'; import { PointInput } from './components/PointInput'; import { ImportFileInput } from './components/ImportFileInput'; import { FlashMessage } from '../shared/FlashMessage'; -import { ComboSearchProps } from '../ComboSearch'; export default function MapEditor({ featureCollection: initialFeatureCollection, url, + adresseSource, options, - autocompleteAnnounceTemplateId, - autocompleteScreenReaderInstructions, champId }: { featureCollection: FeatureCollection; url: string; + adresseSource: string; options: { layers: string[] }; - autocompleteAnnounceTemplateId: ComboSearchProps['announceTemplateId']; - autocompleteScreenReaderInstructions: ComboSearchProps['screenReaderInstructions']; champId: string; }) { const [cadastreEnabled, setCadastreEnabled] = useState(false); @@ -41,15 +38,10 @@ export default function MapEditor({ {error && } - diff --git a/app/views/shared/champs/carte/_show.html.haml b/app/views/shared/champs/carte/_show.html.haml index 4b957d132..98534e434 100644 --- a/app/views/shared/champs/carte/_show.html.haml +++ b/app/views/shared/champs/carte/_show.html.haml @@ -1,4 +1,5 @@ - if champ.geometry? - = react_component("MapReader", { featureCollection: champ.to_feature_collection, options: champ.render_options } ) + %react-fragment.width-100 + = render ReactComponent.new "MapReader", feature_collection: champ.to_feature_collection, options: champ.render_options .geo-areas = render Dossiers::GeoAreasComponent.new(champ:, editing: false) From 4e8b29b21cb075fbc4e372f7589eaf949139dee2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:11:25 +0200 Subject: [PATCH 12/39] refactor(js): remove old code --- .../combobox_component.en.yml | 10 - .../combobox_component.fr.yml | 10 - .../combobox_component.html.haml | 14 - .../editable_champ/combo_search_component.rb | 17 - .../combo_search_component.html.haml | 4 - .../components/ComboAdresseSearch.tsx | 32 -- .../ComboAnnuaireEducationSearch.tsx | 40 -- app/javascript/components/ComboMultiple.tsx | 374 -------------- app/javascript/components/ComboSearch.tsx | 222 --------- app/javascript/components/shared/hooks.ts | 90 ---- .../components/shared/queryClient.ts | 62 --- .../controllers/combobox_controller.ts | 99 ---- app/javascript/shared/combobox-ui.ts | 470 ------------------ app/javascript/shared/combobox.test.ts | 295 ----------- app/javascript/shared/combobox.ts | 300 ----------- .../dsfr/combobox_component_preview.rb | 112 ----- 16 files changed, 2151 deletions(-) delete mode 100644 app/components/dsfr/combobox_component/combobox_component.en.yml delete mode 100644 app/components/dsfr/combobox_component/combobox_component.fr.yml delete mode 100644 app/components/dsfr/combobox_component/combobox_component.html.haml delete mode 100644 app/components/editable_champ/combo_search_component.rb delete mode 100644 app/components/editable_champ/combo_search_component/combo_search_component.html.haml delete mode 100644 app/javascript/components/ComboAdresseSearch.tsx delete mode 100644 app/javascript/components/ComboAnnuaireEducationSearch.tsx delete mode 100644 app/javascript/components/ComboMultiple.tsx delete mode 100644 app/javascript/components/ComboSearch.tsx delete mode 100644 app/javascript/components/shared/hooks.ts delete mode 100644 app/javascript/components/shared/queryClient.ts delete mode 100644 app/javascript/controllers/combobox_controller.ts delete mode 100644 app/javascript/shared/combobox-ui.ts delete mode 100644 app/javascript/shared/combobox.test.ts delete mode 100644 app/javascript/shared/combobox.ts delete mode 100644 spec/components/previews/dsfr/combobox_component_preview.rb diff --git a/app/components/dsfr/combobox_component/combobox_component.en.yml b/app/components/dsfr/combobox_component/combobox_component.en.yml deleted file mode 100644 index e24b49f92..000000000 --- a/app/components/dsfr/combobox_component/combobox_component.en.yml +++ /dev/null @@ -1,10 +0,0 @@ -en: - sr: - results: - zero: No result - one: 1 result - other: "{count} results" - results_with_label: - one: "1 result. {label} is the top result – press Enter to activate" - other: "{count} results. {label} is the top result – press Enter to activate" - selected: "{label} selected" diff --git a/app/components/dsfr/combobox_component/combobox_component.fr.yml b/app/components/dsfr/combobox_component/combobox_component.fr.yml deleted file mode 100644 index dc76ad006..000000000 --- a/app/components/dsfr/combobox_component/combobox_component.fr.yml +++ /dev/null @@ -1,10 +0,0 @@ -fr: - sr: - results: - zero: Aucun résultat - one: 1 résultat - other: "{count} résultats" - results_with_label: - one: "1 résultat. {label} est le premier résultat – appuyez sur Entrée pour sélectionner" - other: "{count} résultats. {label} est le premier résultat – appuyez sur Entrée pour sélectionner" - selected: "{label} sélectionné" diff --git a/app/components/dsfr/combobox_component/combobox_component.html.haml b/app/components/dsfr/combobox_component/combobox_component.html.haml deleted file mode 100644 index 47dc64b3b..000000000 --- a/app/components/dsfr/combobox_component/combobox_component.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.fr-ds-combobox{ data: { controller: 'combobox', allows_custom_value: allows_custom_value, limit: limit } } - .fr-ds-combobox-input - %input{ value: selected_option_label_input_value, **html_input_options } - - if form - = form.hidden_field name, value: selected_option_value_input_value, form: form_id, **@hidden_html_options - - else - %input{ type: 'hidden', name: name, value: selected_option_value_input_value, form: form_id, **@hidden_html_options } - .fr-menu - %ul.fr-menu__list.hidden{ role: 'listbox', hidden: true, id: list_id, data: { turbo_force: :browser, options: options_json, url:, hints: hints_json }.compact } - .sr-only{ aria: { live: 'polite', atomic: 'true' }, data: { turbo_force: :browser } } - %template - %li.fr-menu__item{ role: 'option' } - %slot{ name: 'label' } - = content diff --git a/app/components/editable_champ/combo_search_component.rb b/app/components/editable_champ/combo_search_component.rb deleted file mode 100644 index bbad7a600..000000000 --- a/app/components/editable_champ/combo_search_component.rb +++ /dev/null @@ -1,17 +0,0 @@ -class EditableChamp::ComboSearchComponent < EditableChamp::EditableChampBaseComponent - include ApplicationHelper - - def announce_template_id - @announce_template_id ||= dom_id(@champ, "aria-announce-template") - end - - # NOTE: because this template is called by `render_parent` from a child template, - # as of ViewComponent 2.x translations virtual paths are not properly propagated - # and we can't use the usual component namespacing. Instead we use global translations. - def react_combo_props - { - screenReaderInstructions: t("combo_search_component.screen_reader_instructions"), - announceTemplateId: announce_template_id - } - end -end diff --git a/app/components/editable_champ/combo_search_component/combo_search_component.html.haml b/app/components/editable_champ/combo_search_component/combo_search_component.html.haml deleted file mode 100644 index 9b2e14a56..000000000 --- a/app/components/editable_champ/combo_search_component/combo_search_component.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%template{ id: announce_template_id } - %slot{ "name": "0" }= t("combo_search_component.result_slot_html", count: 0) - %slot{ "name": "1" }= t("combo_search_component.result_slot_html", count: 1) - %slot{ "name": "many" }= t("combo_search_component.result_slot_html", count: 2) diff --git a/app/javascript/components/ComboAdresseSearch.tsx b/app/javascript/components/ComboAdresseSearch.tsx deleted file mode 100644 index f6a34a322..000000000 --- a/app/javascript/components/ComboAdresseSearch.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { QueryClientProvider } from 'react-query'; -import type { FeatureCollection, Geometry } from 'geojson'; - -import ComboSearch, { ComboSearchProps } from './ComboSearch'; -import { queryClient } from './shared/queryClient'; - -type RawResult = FeatureCollection; -type AdresseResult = RawResult['features'][0]; -type ComboAdresseSearchProps = Omit< - ComboSearchProps, - 'minimumInputLength' | 'transformResult' | 'transformResults' | 'scope' ->; - -export default function ComboAdresseSearch({ - allowInputValues = true, - ...props -}: ComboAdresseSearchProps) { - return ( - - - {...props} - allowInputValues={allowInputValues} - scope="adresse" - minimumInputLength={2} - transformResult={({ properties: { label } }) => [label, label, label]} - transformResults={(_, result) => (result as RawResult).features} - debounceDelay={300} - /> - - ); -} diff --git a/app/javascript/components/ComboAnnuaireEducationSearch.tsx b/app/javascript/components/ComboAnnuaireEducationSearch.tsx deleted file mode 100644 index 23fc46ec4..000000000 --- a/app/javascript/components/ComboAnnuaireEducationSearch.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { QueryClientProvider } from 'react-query'; - -import ComboSearch, { ComboSearchProps } from './ComboSearch'; -import { queryClient } from './shared/queryClient'; - -type AnnuaireEducationResult = { - fields: { - identifiant_de_l_etablissement: string; - nom_etablissement: string; - nom_commune: string; - }; -}; - -function transformResults(_: unknown, result: unknown) { - const results = result as { records: AnnuaireEducationResult[] }; - return results.records as AnnuaireEducationResult[]; -} - -export default function ComboAnnuaireEducationSearch( - props: ComboSearchProps -) { - return ( - - [id, `${nom_etablissement}, ${nom_commune} (${id})`]} - /> - - ); -} diff --git a/app/javascript/components/ComboMultiple.tsx b/app/javascript/components/ComboMultiple.tsx deleted file mode 100644 index a4206b9f8..000000000 --- a/app/javascript/components/ComboMultiple.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import { - useMemo, - useState, - useRef, - useContext, - createContext, - useId, - ReactNode, - ChangeEventHandler, - KeyboardEventHandler -} from 'react'; -import { - Combobox, - ComboboxInput, - ComboboxList, - ComboboxOption, - ComboboxPopover -} from '@reach/combobox'; -import '@reach/combobox/styles.css'; -import { matchSorter } from 'match-sorter'; -import { XIcon } from '@heroicons/react/outline'; -import isHotkey from 'is-hotkey'; -import invariant from 'tiny-invariant'; - -import { useDeferredSubmit, useHiddenField } from './shared/hooks'; - -const Context = createContext<{ - onRemove: (value: string) => void; -} | null>(null); - -type Option = [label: string, value: string]; - -function isOptions(options: string[] | Option[]): options is Option[] { - return Array.isArray(options[0]); -} - -const optionLabelByValue = ( - values: string[], - options: Option[], - value: string -): string => { - const maybeOption: Option | undefined = values.includes(value) - ? [value, value] - : options.find(([, optionValue]) => optionValue == value); - return maybeOption ? maybeOption[0] : ''; -}; - -export type ComboMultipleProps = { - options: string[] | Option[]; - id: string; - labelledby: string; - describedby: string; - label: string; - group: string; - name?: string; - selected: string[]; - acceptNewValues?: boolean; -}; - -export default function ComboMultiple({ - options, - id, - labelledby, - describedby, - label, - group, - name = 'value', - selected, - acceptNewValues = false -}: ComboMultipleProps) { - invariant(id || label, 'ComboMultiple: `id` or a `label` are required'); - invariant(group, 'ComboMultiple: `group` is required'); - - const inputRef = useRef(null); - const [term, setTerm] = useState(''); - const [selections, setSelections] = useState(selected); - const [newValues, setNewValues] = useState([]); - const internalId = useId(); - const inputId = id ?? internalId; - const removedLabelledby = `${inputId}-remove`; - const selectedLabelledby = `${inputId}-selected`; - - const optionsWithLabels = useMemo( - () => - isOptions(options) - ? options - : options.filter((o) => o).map((o) => [o, o]), - [options] - ); - - const extraOptions = useMemo( - () => - acceptNewValues && - term && - term.length > 2 && - !optionLabelByValue(newValues, optionsWithLabels, term) - ? [[term, term]] - : [], - [acceptNewValues, term, optionsWithLabels, newValues] - ); - - const extraListOptions = useMemo( - () => - acceptNewValues && term && term.length > 2 && term.includes(';') - ? term.split(';').map((val) => [val.trim(), val.trim()]) - : [], - [acceptNewValues, term] - ); - - const results = useMemo( - () => - [ - ...extraOptions, - ...(term - ? matchSorter( - optionsWithLabels.filter(([label]) => !label.startsWith('--')), - term - ) - : optionsWithLabels) - ].filter(([, value]) => !selections.includes(value)), - [term, selections, extraOptions, optionsWithLabels] - ); - const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name); - const awaitFormSubmit = useDeferredSubmit(hiddenField); - - const handleChange: ChangeEventHandler = (event) => { - setTerm(event.target.value); - }; - - const saveSelection = (fn: (selections: string[]) => string[]) => { - setSelections((selections) => { - selections = fn(selections); - setHiddenFieldValue(JSON.stringify(selections)); - return selections; - }); - }; - - const onSelect = (value: string) => { - const maybeValue = [...extraOptions, ...optionsWithLabels].find( - ([, val]) => val == value - ); - - const maybeValueFromListOptions = extraListOptions.find( - ([, val]) => val == value - ); - - const selectedValue = - term.includes(';') && acceptNewValues - ? maybeValueFromListOptions && maybeValueFromListOptions[1] - : maybeValue && maybeValue[1]; - - if (selectedValue) { - if ( - (acceptNewValues && - extraOptions[0] && - extraOptions[0][0] == selectedValue) || - (acceptNewValues && extraListOptions[0]) - ) { - setNewValues((newValues) => { - const set = new Set(newValues); - set.add(selectedValue); - return [...set]; - }); - } - saveSelection((selections) => { - const set = new Set(selections); - set.add(selectedValue); - return [...set]; - }); - } - setTerm(''); - awaitFormSubmit.done(); - hidePopover(); - }; - - const onRemove = (optionValue: string) => { - if (optionValue) { - saveSelection((selections) => - selections.filter((value) => value != optionValue) - ); - setNewValues((newValues) => - newValues.filter((value) => value != optionValue) - ); - } - inputRef.current?.focus(); - }; - - const onKeyDown: KeyboardEventHandler = (event) => { - if ( - isHotkey('enter', event) || - isHotkey(' ', event) || - isHotkey(',', event) || - isHotkey(';', event) - ) { - if (term.includes(';')) { - for (const val of term.split(';')) { - event.preventDefault(); - onSelect(val.trim()); - } - } else if ( - term && - [...extraOptions, ...optionsWithLabels] - .map(([label]) => label) - .includes(term) - ) { - event.preventDefault(); - onSelect(term); - } - } - }; - - const hidePopover = () => { - document - .querySelector(`[data-reach-combobox-popover-id="${inputId}"]`) - ?.setAttribute('hidden', 'true'); - }; - - const showPopover = () => { - document - .querySelector(`[data-reach-combobox-popover-id="${inputId}"]`) - ?.removeAttribute('hidden'); - }; - - const onBlur = () => { - const shouldSelect = - term && - [...extraOptions, ...optionsWithLabels] - .map(([label]) => label) - .includes(term); - - awaitFormSubmit(() => { - if (term.includes(';')) { - for (const val of term.split(';')) { - onSelect(val.trim()); - } - } else if (shouldSelect) { - onSelect(term); - } else { - hidePopover(); - } - }); - }; - - return ( - - - - désélectionner - -
    - {selections.map((selection) => ( - - {optionLabelByValue(newValues, optionsWithLabels, selection)} - - ))} -
- -
- {results && (results.length > 0 || !acceptNewValues) && ( - - - {results.length === 0 && ( -
  • - Aucun résultat{' '} - -
  • - )} - {results.map(([label, value], index) => { - if (label.startsWith('--')) { - return ; - } - return ( - - {label} - - ); - })} -
    -
    - )} -
    - ); -} - -function ComboboxTokenLabel({ - onRemove, - children -}: { - onRemove: (value: string) => void; - children: ReactNode; -}) { - return ( - -
    {children}
    -
    - ); -} - -function ComboboxSeparator({ value }: { value: string }) { - return ( -
  • - {value.slice(2, -2)} -
  • - ); -} - -function ComboboxToken({ - value, - describedby, - children, - ...props -}: { - value: string; - describedby: string; - children: ReactNode; -}) { - const context = useContext(Context); - invariant(context, 'invalid context'); - const { onRemove } = context; - - return ( -
  • - -
  • - ); -} diff --git a/app/javascript/components/ComboSearch.tsx b/app/javascript/components/ComboSearch.tsx deleted file mode 100644 index 2115d6be6..000000000 --- a/app/javascript/components/ComboSearch.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import React, { - useState, - useEffect, - useRef, - useId, - ChangeEventHandler -} from 'react'; -import { useDebounce } from 'use-debounce'; -import { useQuery } from 'react-query'; -import { - Combobox, - ComboboxInput, - ComboboxPopover, - ComboboxList, - ComboboxOption -} from '@reach/combobox'; -import '@reach/combobox/styles.css'; -import invariant from 'tiny-invariant'; - -import { useDeferredSubmit, useHiddenField, groupId } from './shared/hooks'; - -type TransformResults = (term: string, results: unknown) => Result[]; -type TransformResult = ( - result: Result -) => [key: string, value: string, label?: string]; - -export type ComboSearchProps = { - onChange?: (value: string | null, result?: Result) => void; - value?: string; - scope: string; - scopeExtra?: string; - minimumInputLength: number; - transformResults?: TransformResults; - transformResult: TransformResult; - allowInputValues?: boolean; - id?: string; - describedby?: string; - className?: string; - placeholder?: string; - debounceDelay?: number; - screenReaderInstructions: string; - announceTemplateId: string; -}; - -type QueryKey = readonly [ - scope: string, - term: string, - extra: string | undefined -]; - -function ComboSearch({ - onChange, - value: controlledValue, - scope, - scopeExtra, - minimumInputLength, - transformResult, - allowInputValues = false, - transformResults = (_, results) => results as Result[], - id, - describedby, - screenReaderInstructions, - announceTemplateId, - debounceDelay = 0, - ...props -}: ComboSearchProps) { - invariant(id || onChange, 'ComboSearch: `id` or `onChange` are required'); - - const group = !onChange && id ? groupId(id) : undefined; - const [externalValue, setExternalValue, hiddenField] = useHiddenField(group); - const [, setExternalId] = useHiddenField(group, 'external_id'); - const initialValue = externalValue ? externalValue : controlledValue; - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearchTerm] = useDebounce(searchTerm, debounceDelay); - const [value, setValue] = useState(initialValue); - const resultsMap = useRef< - Record - >({}); - const getLabel = (result: Result) => { - const [, value, label] = transformResult(result); - return label ?? value; - }; - const setExternalValueAndId = (label: string) => { - const { key, value, result } = resultsMap.current[label]; - if (onChange) { - onChange(value, result); - } else { - setExternalId(key); - setExternalValue(value); - } - }; - const awaitFormSubmit = useDeferredSubmit(hiddenField); - - const handleOnChange: ChangeEventHandler = ({ - 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 handleOnSelect = (value: string) => { - setExternalValueAndId(value); - setValue(value); - setSearchTerm(''); - awaitFormSubmit.done(); - }; - - const { isSuccess, data } = useQuery( - [scope, debouncedSearchTerm, scopeExtra], - { - enabled: !!debouncedSearchTerm, - refetchOnMount: false - } - ); - const results = - isSuccess && data ? transformResults(debouncedSearchTerm, data) : []; - - const onBlur = () => { - if (!allowInputValues && isSuccess && results[0]) { - const label = getLabel(results[0]); - awaitFormSubmit(() => { - handleOnSelect(label); - }); - } - }; - - const [announceLive, setAnnounceLive] = useState(''); - const announceTimeout = useRef>(); - const announceTemplate = document.querySelector( - `#${announceTemplateId}` - ); - invariant(announceTemplate, `Missing #${announceTemplateId}`); - - const announceFragment = useRef( - announceTemplate.content.cloneNode(true) as DocumentFragment - ).current; - - useEffect(() => { - if (isSuccess) { - const slot = announceFragment.querySelector( - 'slot[name="' + (results.length <= 1 ? results.length : 'many') + '"]' - ); - - if (!slot) { - return; - } - - const countSlot = - slot.querySelector('slot[name="count"]'); - if (countSlot) { - countSlot.replaceWith(String(results.length)); - } - - setAnnounceLive(slot.textContent ?? ''); - } - - announceTimeout.current = setTimeout(() => { - setAnnounceLive(''); - }, 3000); - - return () => clearTimeout(announceTimeout.current); - }, [announceFragment, results.length, isSuccess]); - - const initInstrId = useId(); - const resultsId = useId(); - - return ( - - - {isSuccess && ( - - {results.length > 0 ? ( - - {results.map((result, index) => { - const label = getLabel(result); - const [key, value] = transformResult(result); - resultsMap.current[label] = { key, value, result }; - return ; - })} - - ) : ( - - Aucun résultat trouvé - - )} - - )} - {!describedby && ( - - {screenReaderInstructions} - - )} -
    - {announceLive} -
    -
    - ); -} - -export default ComboSearch; diff --git a/app/javascript/components/shared/hooks.ts b/app/javascript/components/shared/hooks.ts deleted file mode 100644 index 3574455d4..000000000 --- a/app/javascript/components/shared/hooks.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useRef, useCallback, useMemo, useState } from 'react'; -import { fire } from '@utils'; - -export function useDeferredSubmit(input?: HTMLInputElement): { - (callback: () => void): void; - done: () => void; -} { - const calledRef = useRef(false); - const awaitFormSubmit = useCallback( - (callback: () => void) => { - const form = input?.form; - if (!form) { - return; - } - const interceptFormSubmit = (event: Event) => { - event.preventDefault(); - runCallback(); - - if ( - !Array.from(form.elements).some( - (e) => - e.hasAttribute('data-direct-upload-url') && - 'value' in e && - e.value != '' - ) - ) { - form.submit(); - } - // else: form will be submitted by diret upload once file have been uploaded - }; - calledRef.current = false; - form.addEventListener('submit', interceptFormSubmit); - const runCallback = () => { - form.removeEventListener('submit', interceptFormSubmit); - clearTimeout(timer); - if (!calledRef.current) { - callback(); - } - }; - const timer = setTimeout(runCallback, 400); - }, - [input] - ); - const done = () => { - calledRef.current = true; - }; - return Object.assign(awaitFormSubmit, { done }); -} - -export function groupId(id: string) { - return `#${id.replace(/-input$/, '')}`; -} - -export function useHiddenField( - group?: string, - name = 'value' -): [ - value: string | undefined, - setValue: (value: string) => void, - input: HTMLInputElement | undefined -] { - const hiddenField = useMemo( - () => selectInputInGroup(group, name), - [group, name] - ); - const [value, setValue] = useState(() => hiddenField?.value); - - return [ - value, - (value) => { - if (hiddenField) { - hiddenField.setAttribute('value', value); - setValue(value); - fire(hiddenField, 'change'); - } - }, - hiddenField ?? undefined - ]; -} - -function selectInputInGroup( - group: string | undefined, - name: string -): HTMLInputElement | undefined | null { - if (group) { - return document.querySelector( - `${group} input[type="hidden"][name$="[${name}]"], ${group} input[type="hidden"][name="${name}"]` - ); - } -} diff --git a/app/javascript/components/shared/queryClient.ts b/app/javascript/components/shared/queryClient.ts deleted file mode 100644 index 700dc595d..000000000 --- a/app/javascript/components/shared/queryClient.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { QueryClient, QueryFunction } from 'react-query'; -import { httpRequest, getConfig } from '@utils'; - -const API_EDUCATION_QUERY_LIMIT = 5; -const API_ADRESSE_QUERY_LIMIT = 5; - -const { - autocomplete: { api_adresse_url, api_education_url } -} = getConfig(); - -type QueryKey = readonly [ - scope: string, - term: string, - extra: string | undefined -]; - -function buildURL(scope: string, term: string) { - term = term.replace(/\(|\)/g, ''); - const params = new URLSearchParams(); - let path = ''; - - if (scope == 'adresse') { - path = `${api_adresse_url}/search`; - params.set('q', term); - params.set('limit', `${API_ADRESSE_QUERY_LIMIT}`); - } else if (scope == 'annuaire-education') { - path = `${api_education_url}/search`; - params.set('q', term); - params.set('rows', `${API_EDUCATION_QUERY_LIMIT}`); - params.set('dataset', 'fr-en-annuaire-education'); - } - - return `${path}?${params}`; -} - -const defaultQueryFn: QueryFunction = async ({ - queryKey: [scope, term], - signal -}) => { - // BAN will error with queries less then 3 chars long - if (scope == 'adresse' && term.length < 3) { - return { - type: 'FeatureCollection', - version: 'draft', - features: [], - query: term - }; - } - - const url = buildURL(scope, term); - return httpRequest(url, { csrf: false, signal }).json(); -}; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // we don't really care about global queryFn type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - queryFn: defaultQueryFn as any - } - } -}); diff --git a/app/javascript/controllers/combobox_controller.ts b/app/javascript/controllers/combobox_controller.ts deleted file mode 100644 index 36eddca2c..000000000 --- a/app/javascript/controllers/combobox_controller.ts +++ /dev/null @@ -1,99 +0,0 @@ -import invariant from 'tiny-invariant'; -import { isInputElement, isElement } from '@coldwired/utils'; - -import { Hint } from '../shared/combobox'; -import { ComboboxUI } from '../shared/combobox-ui'; -import { ApplicationController } from './application_controller'; - -export class ComboboxController extends ApplicationController { - #combobox?: ComboboxUI; - - connect() { - const { input, selectedValueInput, valueSlots, list, item, hint } = - this.getElements(); - const hints = JSON.parse(list.dataset.hints ?? '{}') as Record< - string, - string - >; - this.#combobox = new ComboboxUI({ - input, - selectedValueInput, - valueSlots, - list, - item, - hint, - allowsCustomValue: this.element.hasAttribute('data-allows-custom-value'), - limit: this.element.hasAttribute('data-limit') - ? Number(this.element.getAttribute('data-limit')) - : undefined, - getHintText: (hint) => getHintText(hints, hint) - }); - this.#combobox.init(); - } - - disconnect() { - this.#combobox?.destroy(); - } - - private getElements() { - const input = - this.element.querySelector('input[type="text"]'); - const selectedValueInput = this.element.querySelector( - 'input[type="hidden"]' - ); - const valueSlots = this.element.querySelectorAll( - 'input[type="hidden"][data-value-slot]' - ); - const list = this.element.querySelector('[role=listbox]'); - const item = this.element.querySelector('template'); - const hint = - this.element.querySelector('[aria-live]') ?? undefined; - - invariant( - isInputElement(input), - 'ComboboxController requires a input element' - ); - invariant( - isInputElement(selectedValueInput), - 'ComboboxController requires a hidden input element' - ); - invariant( - isElement(list), - 'ComboboxController requires a [role=listbox] element' - ); - invariant( - isElement(item), - 'ComboboxController requires a template element' - ); - - return { input, selectedValueInput, valueSlots, list, item, hint }; - } -} - -function getHintText(hints: Record, hint: Hint): string { - const slot = hints[getSlotName(hint)]; - switch (hint.type) { - case 'empty': - return slot; - case 'selected': - return slot.replace('{label}', hint.label ?? ''); - default: - return slot - .replace('{count}', String(hint.count)) - .replace('{label}', hint.label ?? ''); - } -} - -function getSlotName(hint: Hint): string { - switch (hint.type) { - case 'empty': - return 'empty'; - case 'selected': - return 'selected'; - default: - if (hint.count == 1) { - return hint.label ? 'oneWithLabel' : 'one'; - } - return hint.label ? 'manyWithLabel' : 'many'; - } -} diff --git a/app/javascript/shared/combobox-ui.ts b/app/javascript/shared/combobox-ui.ts deleted file mode 100644 index c55b7359d..000000000 --- a/app/javascript/shared/combobox-ui.ts +++ /dev/null @@ -1,470 +0,0 @@ -import invariant from 'tiny-invariant'; -import { isElement, dispatch, isInputElement } from '@coldwired/utils'; -import { dispatchAction } from '@coldwired/actions'; -import { createPopper, Instance as Popper } from '@popperjs/core'; - -import { - Combobox, - Action, - type State, - type Option, - type Hint, - type Fetcher -} from './combobox'; - -const ctrlBindings = !!navigator.userAgent.match(/Macintosh/); - -export type ComboboxUIOptions = { - input: HTMLInputElement; - selectedValueInput: HTMLInputElement; - list: HTMLUListElement; - item: HTMLTemplateElement; - valueSlots?: HTMLInputElement[] | NodeListOf; - allowsCustomValue?: boolean; - limit?: number; - hint?: HTMLElement; - getHintText?: (hint: Hint) => string; -}; - -export class ComboboxUI implements EventListenerObject { - #combobox?: Combobox; - #popper?: Popper; - #interactingWithList = false; - #mouseOverList = false; - #isComposing = false; - - #input: HTMLInputElement; - #selectedValueInput: HTMLInputElement; - #valueSlots: HTMLInputElement[]; - #list: HTMLUListElement; - #item: HTMLTemplateElement; - #hint?: HTMLElement; - - #getHintText = defaultGetHintText; - #allowsCustomValue: boolean; - #limit?: number; - - #selectedData: Option['data'] = null; - - constructor({ - input, - selectedValueInput, - valueSlots, - list, - item, - hint, - getHintText, - allowsCustomValue, - limit - }: ComboboxUIOptions) { - this.#input = input; - this.#selectedValueInput = selectedValueInput; - this.#valueSlots = valueSlots ? Array.from(valueSlots) : []; - this.#list = list; - this.#item = item; - this.#hint = hint; - this.#getHintText = getHintText ?? defaultGetHintText; - this.#allowsCustomValue = allowsCustomValue ?? false; - this.#limit = limit; - } - - init() { - if (this.#list.dataset.url) { - const fetcher = createFetcher(this.#list.dataset.url); - - this.#list.removeAttribute('data-url'); - - const selected: Option | null = this.#input.value - ? { label: this.#input.value, value: this.#selectedValueInput.value } - : null; - this.#combobox = new Combobox({ - options: fetcher, - selected, - allowsCustomValue: this.#allowsCustomValue, - limit: this.#limit, - render: (state) => this.render(state) - }); - } else { - const selectedValue = this.#selectedValueInput.value; - const options = JSON.parse( - this.#list.dataset.options ?? '[]' - ) as Option[]; - const selected = - options.find(({ value }) => value == selectedValue) ?? null; - - this.#list.removeAttribute('data-options'); - this.#list.removeAttribute('data-selected'); - - this.#combobox = new Combobox({ - options, - selected, - allowsCustomValue: this.#allowsCustomValue, - limit: this.#limit, - render: (state) => this.render(state) - }); - } - - this.#combobox.init(); - - this.#input.addEventListener('blur', this); - this.#input.addEventListener('focus', this); - this.#input.addEventListener('click', this); - this.#input.addEventListener('input', this); - this.#input.addEventListener('keydown', this); - - this.#list.addEventListener('mousedown', this); - this.#list.addEventListener('mouseenter', this); - this.#list.addEventListener('mouseleave', this); - - document.body.addEventListener('mouseup', this); - } - - destroy() { - this.#combobox?.destroy(); - this.#popper?.destroy(); - - this.#input.removeEventListener('blur', this); - this.#input.removeEventListener('focus', this); - this.#input.removeEventListener('click', this); - this.#input.removeEventListener('input', this); - this.#input.removeEventListener('keydown', this); - - this.#list.removeEventListener('mousedown', this); - this.#list.removeEventListener('mouseenter', this); - this.#list.removeEventListener('mouseleave', this); - - document.body.removeEventListener('mouseup', this); - } - - handleEvent(event: Event) { - switch (event.type) { - case 'input': - this.onInputChange(event as InputEvent); - break; - case 'blur': - this.onInputBlur(); - break; - case 'focus': - this.onInputFocus(); - break; - case 'click': - if (event.target == this.#input) { - this.onInputClick(event as MouseEvent); - } else { - this.onListClick(event as MouseEvent); - } - break; - case 'keydown': - this.onKeydown(event as KeyboardEvent); - break; - case 'mousedown': - this.onListMouseDown(); - break; - case 'mouseenter': - this.onListMouseEnter(); - break; - case 'mouseleave': - this.onListMouseLeave(); - break; - case 'mouseup': - this.onBodyMouseUp(event); - break; - case 'compositionstart': - case 'compositionend': - this.#isComposing = event.type == 'compositionstart'; - break; - } - } - - private get combobox() { - invariant(this.#combobox, 'ComboboxUI requires a Combobox instance'); - return this.#combobox; - } - - private render(state: State) { - console.debug('combobox render', state); - switch (state.action) { - case Action.Select: - case Action.Clear: - this.renderSelect(state); - break; - } - this.renderList(state); - this.renderOptionList(state); - this.renderValue(state); - this.renderHintForScreenReader(state.hint); - } - - private renderList(state: State): void { - if (state.open) { - if (!this.#list.hidden) return; - this.#list.hidden = false; - this.#list.classList.remove('hidden'); - this.#list.addEventListener('click', this); - - this.#input.setAttribute('aria-expanded', 'true'); - - this.#input.addEventListener('compositionstart', this); - this.#input.addEventListener('compositionend', this); - - this.#popper = createPopper(this.#input, this.#list, { - placement: 'bottom-start' - }); - } else { - if (this.#list.hidden) return; - this.#list.hidden = true; - this.#list.classList.add('hidden'); - this.#list.removeEventListener('click', this); - - this.#input.setAttribute('aria-expanded', 'false'); - this.#input.removeEventListener('compositionstart', this); - this.#input.removeEventListener('compositionend', this); - - this.#popper?.destroy(); - this.#interactingWithList = false; - } - } - - private renderValue(state: State): void { - if (this.#input.value != state.inputValue) { - this.#input.value = state.inputValue; - } - this.dispatchChange(() => { - if (this.#selectedValueInput.value != state.inputValue) { - if (state.allowsCustomValue || !state.inputValue) { - this.#selectedValueInput.value = state.inputValue; - } - } - return state.selection?.data; - }); - } - - private renderSelect(state: State): void { - this.dispatchChange(() => { - this.#selectedValueInput.value = state.selection?.value ?? ''; - this.#input.value = state.selection?.label ?? ''; - return state.selection?.data; - }); - } - - private renderOptionList(state: State): void { - const html = state.options - .map(({ label, value }) => { - const fragment = this.#item.content.cloneNode(true) as DocumentFragment; - const item = fragment.querySelector('li'); - if (item) { - item.id = optionId(value); - item.setAttribute('data-turbo-force', 'server'); - if (state.focused?.value == value) { - item.setAttribute('aria-selected', 'true'); - } else { - item.removeAttribute('aria-selected'); - } - item.setAttribute('data-value', value); - item.querySelector('slot[name="label"]')?.replaceWith(label); - return item.outerHTML; - } - return ''; - }) - .join(''); - - dispatchAction({ targets: this.#list, action: 'update', fragment: html }); - - if (state.focused) { - const id = optionId(state.focused.value); - const item = this.#list.querySelector(`#${id}`); - this.#input.setAttribute('aria-activedescendant', id); - if (item) { - scrollTo(this.#list, item); - } - } else { - this.#input.removeAttribute('aria-activedescendant'); - } - } - - private renderHintForScreenReader(hint: Hint | null): void { - if (this.#hint) { - if (hint) { - this.#hint.textContent = this.#getHintText(hint); - } else { - this.#hint.textContent = ''; - } - } - } - - private dispatchChange(cb: () => Option['data']): void { - const value = this.#selectedValueInput.value; - const data = cb(); - if (value != this.#selectedValueInput.value || data != this.#selectedData) { - this.#selectedData = data; - for (const input of this.#valueSlots) { - switch (input.dataset.valueSlot) { - case 'value': - input.value = this.#selectedValueInput.value; - break; - case 'label': - input.value = this.#input.value; - break; - case 'data:string': - input.value = data ? String(data) : ''; - break; - case 'data': - input.value = data ? JSON.stringify(data) : ''; - break; - } - } - console.debug('combobox change', this.#selectedValueInput.value); - dispatch('change', { - target: this.#selectedValueInput, - detail: data ? { data } : undefined - }); - } - } - - private onKeydown(event: KeyboardEvent): void { - if (event.shiftKey || event.metaKey || event.altKey) return; - if (!ctrlBindings && event.ctrlKey) return; - if (this.#isComposing) return; - - if (this.combobox.keyboard(event.key)) { - event.preventDefault(); - event.stopPropagation(); - } - } - - private onInputClick(event: MouseEvent): void { - const rect = this.#input.getBoundingClientRect(); - const clickOnArrow = - event.clientX >= rect.right - 40 && - event.clientX <= rect.right && - event.clientY >= rect.top && - event.clientY <= rect.bottom; - - if (clickOnArrow) { - this.combobox.toggle(); - } - } - - private onListClick(event: MouseEvent): void { - if (isElement(event.target)) { - const element = event.target.closest('[role="option"]'); - if (element) { - const value = element.getAttribute('data-value')?.trim(); - if (value) { - this.combobox.select(value); - } - } - } - } - - private onInputFocus(): void { - this.combobox.focus(); - } - - private onInputBlur(): void { - if (!this.#interactingWithList) { - this.combobox.close(); - } - } - - private onInputChange(event: InputEvent): void { - if (isInputElement(event.target)) { - this.combobox.input(event.target.value); - } - } - - private onListMouseDown(): void { - this.#interactingWithList = true; - } - - private onBodyMouseUp(event: Event): void { - if ( - this.#interactingWithList && - !this.#mouseOverList && - isElement(event.target) && - event.target != this.#list && - !this.#list.contains(event.target) - ) { - this.combobox.close(); - } - } - - private onListMouseEnter(): void { - this.#mouseOverList = true; - } - - private onListMouseLeave(): void { - this.#mouseOverList = false; - } -} - -function scrollTo(container: HTMLElement, target: HTMLElement): void { - if (!inViewport(container, target)) { - container.scrollTop = target.offsetTop; - } -} - -function inViewport(container: HTMLElement, element: HTMLElement): boolean { - const scrollTop = container.scrollTop; - const containerBottom = scrollTop + container.clientHeight; - const top = element.offsetTop; - const bottom = top + element.clientHeight; - return top >= scrollTop && bottom <= containerBottom; -} - -function optionId(value: string) { - return `option-${value - .toLowerCase() - // Replace spaces and special characters with underscores - .replace(/[^a-z0-9]/g, '_') - // Remove non-alphanumeric characters at start and end - .replace(/^[^a-z]+|[^\w]$/g, '')}`; -} - -function defaultGetHintText(hint: Hint): string { - switch (hint.type) { - case 'results': - if (hint.label) { - return `${hint.count} results. ${hint.label} is the top result: press Enter to activate.`; - } - return `${hint.count} results.`; - case 'empty': - return 'No results.'; - case 'selected': - return `${hint.label} selected.`; - } -} - -function createFetcher(source: string, param = 'q'): Fetcher { - const url = new URL(source, location.href); - - const fetcher: Fetcher = (term: string, options) => { - url.searchParams.set(param, term); - return fetch(url.toString(), { - headers: { accept: 'application/json' }, - signal: options?.signal - }).then((response) => { - if (response.ok) { - return response.json(); - } - return []; - }); - }; - - return async (term: string, options) => { - await wait(500, options?.signal); - return fetcher(term, options); - }; -} - -function wait(ms: number, signal?: AbortSignal) { - return new Promise((resolve, reject) => { - const abort = () => reject(new DOMException('Aborted', 'AbortError')); - if (signal?.aborted) { - abort(); - } else { - signal?.addEventListener('abort', abort); - setTimeout(resolve, ms); - } - }); -} diff --git a/app/javascript/shared/combobox.test.ts b/app/javascript/shared/combobox.test.ts deleted file mode 100644 index 45633b997..000000000 --- a/app/javascript/shared/combobox.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { suite, test, beforeEach, expect } from 'vitest'; -import { matchSorter } from 'match-sorter'; - -import { Combobox, Option, State } from './combobox'; - -suite('Combobox', () => { - const options: Option[] = - 'Fraises,Myrtilles,Framboises,Mûres,Canneberges,Groseilles,Baies de sureau,Mûres blanches,Baies de genièvre,Baies d’açaï' - .split(',') - .map((label) => ({ label, value: label })); - - let combobox: Combobox; - let currentState: State; - - suite('single select without custom value', () => { - suite('with default selection', () => { - beforeEach(() => { - combobox = new Combobox({ - options, - selected: options.at(0) ?? null, - render: (state) => { - currentState = state; - } - }); - combobox.init(); - }); - - test('open select box and select option with click', () => { - expect(currentState.open).toBeFalsy(); - expect(currentState.loading).toBe(null); - expect(currentState.selection?.label).toBe('Fraises'); - - combobox.open(); - expect(currentState.open).toBeTruthy(); - - combobox.select('Mûres'); - expect(currentState.selection?.label).toBe('Mûres'); - expect(currentState.open).toBeFalsy(); - }); - - test('open select box and select option with enter', () => { - expect(currentState.open).toBeFalsy(); - expect(currentState.selection?.label).toBe('Fraises'); - - combobox.keyboard('ArrowDown'); - expect(currentState.open).toBeTruthy(); - expect(currentState.selection?.label).toBe('Fraises'); - expect(currentState.focused?.label).toBe('Fraises'); - - combobox.keyboard('ArrowDown'); - expect(currentState.selection?.label).toBe('Fraises'); - expect(currentState.focused?.label).toBe('Myrtilles'); - - combobox.keyboard('Enter'); - expect(currentState.selection?.label).toBe('Myrtilles'); - expect(currentState.open).toBeFalsy(); - - combobox.keyboard('Enter'); - expect(currentState.selection?.label).toBe('Myrtilles'); - expect(currentState.open).toBeFalsy(); - }); - - test('open select box and select option with tab', () => { - combobox.keyboard('ArrowDown'); - combobox.keyboard('ArrowDown'); - - combobox.keyboard('Tab'); - expect(currentState.selection?.label).toBe('Myrtilles'); - expect(currentState.open).toBeFalsy(); - expect(currentState.hint).toEqual({ - type: 'selected', - label: 'Myrtilles' - }); - }); - - test('do not open select box on focus', () => { - combobox.focus(); - expect(currentState.open).toBeFalsy(); - }); - }); - - suite('empty', () => { - beforeEach(() => { - combobox = new Combobox({ - options, - selected: null, - render: (state) => { - currentState = state; - } - }); - combobox.init(); - }); - - test('open select box on focus', () => { - combobox.focus(); - expect(currentState.open).toBeTruthy(); - }); - - suite('open', () => { - beforeEach(() => { - combobox.open(); - }); - - test('if tab on empty input nothing is selected', () => { - expect(currentState.open).toBeTruthy(); - expect(currentState.selection).toBeNull(); - combobox.keyboard('Tab'); - - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - }); - - test('if enter on empty input nothing is selected', () => { - expect(currentState.open).toBeTruthy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Enter'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - }); - }); - - suite('closed', () => { - test('if tab on empty input nothing is selected', () => { - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Tab'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - }); - - test('if enter on empty input nothing is selected', () => { - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Enter'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - }); - }); - - test('type exact match and press enter', () => { - combobox.input('Baies'); - expect(currentState.open).toBeTruthy(); - expect(currentState.selection).toBeNull(); - expect(currentState.options.length).toEqual(3); - - combobox.keyboard('Enter'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection?.label).toBe('Baies d’açaï'); - }); - - test('type exact match and press tab', () => { - combobox.input('Baies'); - expect(currentState.open).toBeTruthy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Tab'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection?.label).toBe('Baies d’açaï'); - expect(currentState.inputValue).toEqual('Baies d’açaï'); - }); - - test('type non matching input and press enter', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Enter'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual(''); - }); - - test('type non matching input and press tab', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Tab'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual(''); - }); - - test('type non matching input and close', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.close(); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual(''); - }); - - test('focus should circle', () => { - combobox.input('Baie'); - expect(currentState.open).toBeTruthy(); - expect(currentState.options.map(({ label }) => label)).toEqual([ - 'Baies d’açaï', - 'Baies de genièvre', - 'Baies de sureau' - ]); - expect(currentState.focused).toBeNull(); - combobox.keyboard('ArrowDown'); - expect(currentState.focused?.label).toBe('Baies d’açaï'); - combobox.keyboard('ArrowDown'); - expect(currentState.focused?.label).toBe('Baies de genièvre'); - combobox.keyboard('ArrowDown'); - expect(currentState.focused?.label).toBe('Baies de sureau'); - combobox.keyboard('ArrowDown'); - expect(currentState.focused?.label).toBe('Baies d’açaï'); - combobox.keyboard('ArrowUp'); - expect(currentState.focused?.label).toBe('Baies de sureau'); - }); - }); - }); - - suite('single select with custom value', () => { - beforeEach(() => { - combobox = new Combobox({ - options, - selected: null, - allowsCustomValue: true, - render: (state) => { - currentState = state; - } - }); - combobox.init(); - }); - - test('type non matching input and press enter', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Enter'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual('toto'); - }); - - test('type non matching input and press tab', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.keyboard('Tab'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual('toto'); - }); - - test('type non matching input and close', () => { - combobox.input('toto'); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - - combobox.close(); - expect(currentState.open).toBeFalsy(); - expect(currentState.selection).toBeNull(); - expect(currentState.inputValue).toEqual('toto'); - }); - }); - - suite('single select with fetcher', () => { - beforeEach(() => { - combobox = new Combobox({ - options: (term: string) => - Promise.resolve(matchSorter(options, term, { keys: ['value'] })), - selected: null, - render: (state) => { - currentState = state; - } - }); - combobox.init(); - }); - - test('type and get options from fetcher', async () => { - expect(currentState.open).toBeFalsy(); - expect(currentState.loading).toBe(false); - - const result = combobox.input('Baies'); - - expect(currentState.loading).toBe(true); - await result; - expect(currentState.loading).toBe(false); - expect(currentState.open).toBeTruthy(); - expect(currentState.selection).toBeNull(); - expect(currentState.options.length).toEqual(3); - }); - }); -}); diff --git a/app/javascript/shared/combobox.ts b/app/javascript/shared/combobox.ts deleted file mode 100644 index b5c82c524..000000000 --- a/app/javascript/shared/combobox.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { matchSorter } from 'match-sorter'; - -export enum Action { - Init = 'init', - Open = 'open', - Close = 'close', - Navigate = 'navigate', - Select = 'select', - Clear = 'clear', - Update = 'update' -} -export type Option = { value: string; label: string; data?: unknown }; -export type Hint = - | { - type: 'results'; - label: string | null; - count: number; - } - | { type: 'empty' } - | { type: 'selected'; label: string }; -export type State = { - action: Action; - open: boolean; - inputValue: string; - focused: Option | null; - selection: Option | null; - options: Option[]; - allowsCustomValue: boolean; - hint: Hint | null; - loading: boolean | null; -}; - -export type Fetcher = ( - term: string, - options?: { signal: AbortSignal } -) => Promise; - -export class Combobox { - #allowsCustomValue = false; - #limit?: number; - #open = false; - #inputValue = ''; - #selectedOption: Option | null = null; - #focusedOption: Option | null = null; - #options: Option[] = []; - #visibleOptions: Option[] = []; - #render: (state: State) => void; - #fetcher: Fetcher | null; - #abortController?: AbortController | null; - - constructor({ - options, - selected, - allowsCustomValue, - limit, - render - }: { - options: Option[] | Fetcher; - selected: Option | null; - allowsCustomValue?: boolean; - limit?: number; - render: (state: State) => void; - }) { - this.#allowsCustomValue = allowsCustomValue ?? false; - this.#limit = limit; - this.#options = Array.isArray(options) ? options : []; - this.#fetcher = Array.isArray(options) ? null : options; - this.#selectedOption = selected; - if (this.#selectedOption) { - this.#inputValue = this.#selectedOption.label; - } - this.#render = render; - } - - init(): void { - this.#visibleOptions = this._filterOptions(); - this._render(Action.Init); - } - - destroy(): void { - this.#render = () => null; - } - - navigate(indexDiff: -1 | 1 = 1): void { - const focusIndex = this._focusedOptionIndex; - const lastIndex = this.#visibleOptions.length - 1; - - let indexOfItem = indexDiff == 1 ? 0 : lastIndex; - if (focusIndex == lastIndex && indexDiff == 1) { - indexOfItem = 0; - } else if (focusIndex == 0 && indexDiff == -1) { - indexOfItem = lastIndex; - } else if (focusIndex == -1) { - indexOfItem = 0; - } else { - indexOfItem = focusIndex + indexDiff; - } - - this.#focusedOption = this.#visibleOptions.at(indexOfItem) ?? null; - - this._render(Action.Navigate); - } - - select(value?: string): boolean { - const maybeValue = this._nextSelectValue(value); - if (!maybeValue) { - this.close(); - return false; - } - - const option = this.#visibleOptions.find( - (option) => option.value.trim() == maybeValue.trim() - ); - if (!option) return false; - - this.#selectedOption = option; - this.#focusedOption = null; - this.#inputValue = option.label; - this.#open = false; - this.#visibleOptions = this._filterOptions(); - - this._render(Action.Select); - return true; - } - - async input(value: string) { - if (this.#inputValue == value) return; - - this.#inputValue = value; - - if (this.#fetcher) { - this.#abortController?.abort(); - this.#abortController = new AbortController(); - this._render(Action.Update); - this.#options = await this.#fetcher(value, { - signal: this.#abortController.signal - }).catch(() => []); - this.#abortController = null; - this._render(Action.Update); - - this.#selectedOption = null; - } else { - this.#selectedOption = null; - } - - this.#visibleOptions = this._filterOptions(); - - if (this.#visibleOptions.length > 0) { - if (!this.#open) { - this.open(); - } else { - this._render(Action.Update); - } - } else if (this.#allowsCustomValue) { - this.#open = false; - this.#focusedOption = null; - this._render(Action.Close); - } else { - this._render(Action.Update); - } - } - - keyboard(key: string) { - switch (key) { - case 'Enter': - case 'Tab': - return this.select(); - case 'Escape': - this.close(); - return true; - case 'ArrowDown': - if (this.#open) { - this.navigate(1); - } else { - this.open(); - } - return true; - case 'ArrowUp': - if (this.#open) { - this.navigate(-1); - } else { - this.open(); - } - return true; - } - } - - clear() { - if (!this.#inputValue && !this.#selectedOption) return; - this.#inputValue = ''; - this.#selectedOption = this.#focusedOption = null; - this.#visibleOptions = this.#options; - this.#visibleOptions = this._filterOptions(); - this._render(Action.Clear); - } - - open() { - if (this.#open || this.#visibleOptions.length == 0) return; - this.#open = true; - this.#focusedOption = this.#selectedOption; - this._render(Action.Open); - } - - close() { - this.#open = false; - this.#focusedOption = null; - if (!this.#allowsCustomValue && !this.#selectedOption) { - this.#inputValue = ''; - } - this.#visibleOptions = this._filterOptions(); - this._render(Action.Close); - } - - focus() { - if (this.#open) return; - if (this.#selectedOption) return; - - this.open(); - } - - toggle() { - this.#open ? this.close() : this.open(); - } - - private _nextSelectValue(value?: string): string | false { - if (value) { - return value; - } - if (this.#focusedOption && this._focusedOptionIndex != -1) { - return this.#focusedOption.value; - } - if (this.#allowsCustomValue) { - return false; - } - if (this.#inputValue.length > 0 && !this.#selectedOption) { - return this.#visibleOptions.at(0)?.value ?? false; - } - return false; - } - - private _filterOptions(): Option[] { - const emptyOrSelected = - !this.#inputValue || this.#inputValue == this.#selectedOption?.value; - const options = emptyOrSelected - ? this.#options - : matchSorter(this.#options, this.#inputValue, { - keys: ['label'] - }); - - if (this.#limit) { - return options.slice(0, this.#limit); - } - return options; - } - - private get _focusedOptionIndex(): number { - if (this.#focusedOption) { - return this.#visibleOptions.indexOf(this.#focusedOption); - } - return -1; - } - - private _render(action: Action): void { - this.#render(this._getState(action)); - } - - private _getState(action: Action): State { - const state = { - action, - open: this.#open, - options: this.#visibleOptions, - inputValue: this.#inputValue, - focused: this.#focusedOption, - selection: this.#selectedOption, - allowsCustomValue: this.#allowsCustomValue, - hint: null, - loading: this.#abortController ? true : this.#fetcher ? false : null - }; - - return { ...state, hint: this._getFeedback(state) }; - } - - private _getFeedback(state: State): Hint | null { - const count = state.options.length; - if (state.action == Action.Open || state.action == Action.Update) { - if (!state.selection) { - const defaultOption = state.options.at(0); - if (defaultOption) { - return { type: 'results', label: defaultOption.label, count }; - } else if (count > 0) { - return { type: 'results', label: null, count }; - } - return { type: 'empty' }; - } - } else if (state.action == Action.Select && state.selection) { - return { type: 'selected', label: state.selection.label }; - } - return null; - } -} diff --git a/spec/components/previews/dsfr/combobox_component_preview.rb b/spec/components/previews/dsfr/combobox_component_preview.rb deleted file mode 100644 index 57367fd28..000000000 --- a/spec/components/previews/dsfr/combobox_component_preview.rb +++ /dev/null @@ -1,112 +0,0 @@ -class Dsfr::ComboboxComponentPreview < ViewComponent::Preview - OPTIONS = [ - 'Cheddar', - 'Brie', - 'Mozzarella', - 'Gouda', - 'Swiss', - 'Parmesan', - 'Feta', - 'Blue cheese', - 'Camembert', - 'Monterey Jack', - 'Roquefort', - 'Provolone', - 'Colby', - 'Havarti', - 'Ricotta', - 'Pepper Jack', - 'Muenster', - 'Fontina', - 'Limburger', - 'Asiago', - 'Cottage cheese', - 'Emmental', - 'Mascarpone', - 'Taleggio', - 'Gruyere', - 'Edam', - 'Pecorino Romano', - 'Manchego', - 'Halloumi', - 'Jarlsberg', - 'Munster', - 'Stilton', - 'Gorgonzola', - 'Queso blanco', - 'Queso fresco', - 'Queso de bola', - 'Queso de cabra', - 'Queso panela', - 'Queso Oaxaca', - 'Queso Chihuahua', - 'Queso manchego', - 'Queso de bola', - 'Queso de bola de cabra', - 'Queso de bola de vaca', - 'Queso de bola de oveja', - 'Queso de bola de mezcla', - 'Queso de bola de leche cruda', - 'Queso de bola de leche pasteurizada', - 'Queso de bola de leche de cabra', - 'Queso de bola de leche de vaca', - 'Queso de bola de leche de oveja', - 'Queso de bola de leche de mezcla', - 'Burrata', - 'Scamorza', - 'Caciocavallo', - 'Provolone piccante', - 'Pecorino sardo', - 'Pecorino toscano', - 'Pecorino siciliano', - 'Pecorino calabrese', - 'Pecorino moliterno', - 'Pecorino di fossa', - 'Pecorino di filiano', - 'Pecorino di pienza', - 'Pecorino di grotta', - 'Pecorino di capra', - 'Pecorino di mucca', - 'Pecorino di pecora', - 'Pecorino di bufala', - 'Cacio di bosco', - 'Cacio di roma', - 'Cacio di fossa', - 'Cacio di tricarico', - 'Cacio di cavallo', - 'Cacio di capra', - 'Cacio di mucca', - 'Cacio di pecora', - 'Cacio di bufala', - 'Taleggio di capra', - 'Taleggio di mucca', - 'Taleggio di pecora', - 'Taleggio di bufala', - 'Bel Paese', - 'Crescenza', - 'Stracchino', - 'Robiola', - 'Toma', - 'Bra', - 'Castelmagno', - 'Raschera', - 'Montasio', - 'Piave', - 'Bitto', - 'Quartirolo Lombardo', - 'Formaggella del Luinese', - 'Formaggella della Val Vigezzo', - 'Formaggella della Valle Grana', - 'Formaggella della Val Bognanco', - 'Formaggella della Val d’Intelvi', - 'Formaggella della Val Gerola' - ] - - def simple_select_with_options - render Dsfr::ComboboxComponent.new(options: OPTIONS, selected: OPTIONS.sample, input_html_options: { name: :value, id: 'simple-select', class: 'width-33' }) - end - - def simple_select_with_options_and_allows_custom_value - render Dsfr::ComboboxComponent.new(options: OPTIONS, selected: OPTIONS.sample, allows_custom_value: true, input_html_options: { id: 'simple-select', class: 'width-33', name: :value }) - end -end From cb01f15570dfec2d3dc6e185a9309f6e4b71708e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 21:44:41 +0200 Subject: [PATCH 13/39] refactor(champ): update champ commune --- .../editable_champ/communes_component.rb | 11 +++++ .../communes_component.html.haml | 4 +- .../data_sources/commune_controller.rb | 17 +++++--- .../instructeurs/dossiers_controller.rb | 1 + app/controllers/users/dossiers_controller.rb | 1 + app/models/champs/commune_champ.rb | 41 ++++++++++++++----- spec/models/champs/commune_champ_spec.rb | 21 ++++++---- 7 files changed, 71 insertions(+), 25 deletions(-) diff --git a/app/components/editable_champ/communes_component.rb b/app/components/editable_champ/communes_component.rb index 80fbb70e9..3728cc30a 100644 --- a/app/components/editable_champ/communes_component.rb +++ b/app/components/editable_champ/communes_component.rb @@ -4,4 +4,15 @@ class EditableChamp::CommunesComponent < EditableChamp::EditableChampBaseCompone def dsfr_input_classname 'fr-select' end + + def react_props + react_input_opts(id: @champ.input_id, + class: 'fr-mt-1w', + name: @form.field_name(:code), + selected_key: @champ.selected, + items: @champ.selected_items, + loader: data_sources_data_source_commune_path(with_combined_code: true), + limit: 20, + minimum_input_length: 2) + end end diff --git a/app/components/editable_champ/communes_component/communes_component.html.haml b/app/components/editable_champ/communes_component/communes_component.html.haml index 709c87d93..5c2f652de 100644 --- a/app/components/editable_champ/communes_component/communes_component.html.haml +++ b/app/components/editable_champ/communes_component/communes_component.html.haml @@ -1,2 +1,2 @@ -= render Dsfr::ComboboxComponent.new form: @form, url: data_sources_data_source_commune_path, selected: [@champ.to_s, @champ.selected], limit: 20, input_html_options: { name: :external_id, id: @champ.input_id, class: 'fr-select', describedby: @champ.describedby_id } do - = @form.hidden_field :code_postal, data: { value_slot: 'data:string' } +%react-fragment + = render ReactComponent.new "ComboBox/RemoteComboBox", **react_props diff --git a/app/controllers/data_sources/commune_controller.rb b/app/controllers/data_sources/commune_controller.rb index 825d3f7c4..49884399e 100644 --- a/app/controllers/data_sources/commune_controller.rb +++ b/app/controllers/data_sources/commune_controller.rb @@ -61,11 +61,18 @@ class DataSources::CommuneController < ApplicationController else [item] end.map do |item| - { - label: "#{item[:name]} (#{item[:postal_code]})", - value: item[:code], - data: item[:postal_code] - } + if params[:with_combined_code].present? + { + label: "#{item[:name]} (#{item[:postal_code]})", + value: "#{item[:code]}-#{item[:postal_code]}" + } + else + { + label: "#{item[:name]} (#{item[:postal_code]})", + value: item[:code], + data: item[:postal_code] + } + end end end end diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 189477bff..884309712 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -401,6 +401,7 @@ module Instructeurs :value, :value_other, :external_id, + :code, :primary_value, :secondary_value, :numero_allocataire, diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 3e322e618..f22896053 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -494,6 +494,7 @@ module Users :value, :value_other, :external_id, + :code, :primary_value, :secondary_value, :numero_allocataire, diff --git a/app/models/champs/commune_champ.rb b/app/models/champs/commune_champ.rb index be62ce968..37d8ad70e 100644 --- a/app/models/champs/commune_champ.rb +++ b/app/models/champs/commune_champ.rb @@ -28,10 +28,6 @@ class Champs::CommuneChamp < Champs::TextChamp code_postal.present? end - def code_postal=(value) - super(value&.gsub(/[[:space:]]/, '')) - end - alias postal_code code_postal def name @@ -43,7 +39,36 @@ class Champs::CommuneChamp < Champs::TextChamp end def selected - code + code? ? "#{code}-#{code_postal}" : nil + end + + def selected_items + if code? + [{ label: to_s, value: selected }] + else + [] + end + end + + def code=(code) + if code.blank? + self.code_departement = nil + self.code_postal = nil + self.external_id = nil + self.value = nil + elsif code.match?(/-/) + codes = code.split('-') + self.external_id = codes.first + self.code_postal = codes.second + else + self.external_id = code + end + end + + private + + def safe_to_s + value.present? ? value.to_s : '' end def communes @@ -54,12 +79,6 @@ class Champs::CommuneChamp < Champs::TextChamp end end - private - - def safe_to_s - value.present? ? value.to_s : '' - end - def on_codes_change return if !code? diff --git a/spec/models/champs/commune_champ_spec.rb b/spec/models/champs/commune_champ_spec.rb index 76051c0c3..87d726dd6 100644 --- a/spec/models/champs/commune_champ_spec.rb +++ b/spec/models/champs/commune_champ_spec.rb @@ -5,7 +5,7 @@ describe Champs::CommuneChamp do let(:champ) { create(:champ_communes, code_postal:, external_id: code_insee) } describe 'value' do - it 'with code_postal' do + it 'find commune' do expect(champ.to_s).to eq('Châteldon (63290)') expect(champ.name).to eq('Châteldon') expect(champ.external_id).to eq(code_insee) @@ -15,15 +15,22 @@ describe Champs::CommuneChamp do expect(champ.for_export(:value)).to eq 'Châteldon (63290)' expect(champ.for_export(:code)).to eq '63102' expect(champ.for_export(:departement)).to eq '63 – Puy-de-Dôme' - expect(champ.communes.size).to eq(8) end - end - describe 'code_postal with spaces' do - let(:code_postal) { ' 63 2 90  ' } + context 'with code' do + let(:champ) { create(:champ_communes, code: '63102-63290') } - it 'with code_postal' do - expect(champ.communes.size).to eq(8) + it 'find commune' do + expect(champ.to_s).to eq('Châteldon (63290)') + expect(champ.name).to eq('Châteldon') + expect(champ.external_id).to eq(code_insee) + expect(champ.code).to eq(code_insee) + expect(champ.code_departement).to eq(code_departement) + expect(champ.code_postal).to eq(code_postal) + expect(champ.for_export(:value)).to eq 'Châteldon (63290)' + expect(champ.for_export(:code)).to eq '63102' + expect(champ.for_export(:departement)).to eq '63 – Puy-de-Dôme' + end end end end From f0f88ef3f0adda688eccc9b2d02ba5bc1f18ead8 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 7 May 2024 11:25:37 +0200 Subject: [PATCH 14/39] refactor(champ): update champ drop_down_list --- .../editable_champ/drop_down_list_component.rb | 9 +++++++++ .../drop_down_list_component.html.haml | 3 ++- app/models/champ.rb | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/components/editable_champ/drop_down_list_component.rb b/app/components/editable_champ/drop_down_list_component.rb index 0f92a95bc..04f901550 100644 --- a/app/components/editable_champ/drop_down_list_component.rb +++ b/app/components/editable_champ/drop_down_list_component.rb @@ -23,4 +23,13 @@ class EditableChamp::DropDownListComponent < EditableChamp::EditableChampBaseCom max_length = 100 @champ.enabled_non_empty_options.any? { _1.size > max_length } end + + def react_props + react_input_opts(id: @champ.input_id, + class: 'fr-mt-1w', + name: @form.field_name(:value), + selected_key: @champ.selected, + items: @champ.enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1 : [_1, _1] }, + empty_filter_key: @champ.drop_down_other? ? Champs::DropDownListChamp::OTHER : nil) + end end diff --git a/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml b/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml index c2268eba0..3ce73c3ee 100644 --- a/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml +++ b/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml @@ -18,7 +18,8 @@ %label.fr-label{ for: dom_id(@champ, "radio_option_other") } = t('shared.champs.drop_down_list.other') - elsif @champ.render_as_combobox? - = render Dsfr::ComboboxComponent.new form: @form, options: @champ.enabled_non_empty_options(other: true), selected: @champ.selected, input_html_options: { name: :value, id: @champ.input_id, class: select_class_names, describedby: @champ.describedby_id } + %react-fragment + = render ReactComponent.new "ComboBox/SingleComboBox", **react_props - else = @form.select :value, @champ.enabled_non_empty_options(other: true), diff --git a/app/models/champ.rb b/app/models/champ.rb index 093829236..c144fb470 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -285,7 +285,7 @@ class Champ < ApplicationRecord return if value.nil? return if value.present? && !value.include?("\u0000") - self.value = value.delete("\u0000") + write_attribute(:value, value.delete("\u0000")) end class NotImplemented < ::StandardError From dc6af4fb8508fe89869b6ed660952de40ddfec4e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 7 May 2024 11:28:14 +0200 Subject: [PATCH 15/39] refactor(champ): update champ chorus --- .../procedure/chorus_form_component.rb | 30 +++++++++++++++++-- .../chorus_form_component.html.haml | 12 ++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/components/procedure/chorus_form_component.rb b/app/components/procedure/chorus_form_component.rb index 0602a9ccd..19298b775 100644 --- a/app/components/procedure/chorus_form_component.rb +++ b/app/components/procedure/chorus_form_component.rb @@ -14,6 +14,20 @@ class Procedure::ChorusFormComponent < ApplicationComponent } end + def selected_key(attribute_name) + items(attribute_name).first&.dig(:value) + end + + def items(attribute_name) + label = format_displayed_value(attribute_name) + data = format_hidden_value(attribute_name) + if label.present? + [{ label:, value: label, data: }] + else + [] + end + end + def format_displayed_value(attribute_name) case attribute_name when :centre_de_cout @@ -30,13 +44,23 @@ class Procedure::ChorusFormComponent < ApplicationComponent def format_hidden_value(attribute_name) case attribute_name when :centre_de_cout - @chorus_configuration.centre_de_cout.to_json + @chorus_configuration.centre_de_cout when :domaine_fonctionnel - @chorus_configuration.domaine_fonctionnel.to_json + @chorus_configuration.domaine_fonctionnel when :referentiel_de_programmation - @chorus_configuration.referentiel_de_programmation.to_json + @chorus_configuration.referentiel_de_programmation else raise 'unknown attribute_name' end end + + def react_props(name, chorus_configuration_attribute, datasource_endpoint) + { + name:, + selected_key: selected_key(chorus_configuration_attribute), + items: items(chorus_configuration_attribute), + loader: datasource_endpoint, + id: chorus_configuration_attribute + } + end end diff --git a/app/components/procedure/chorus_form_component/chorus_form_component.html.haml b/app/components/procedure/chorus_form_component/chorus_form_component.html.haml index 3fe78a9d3..df2a7131e 100644 --- a/app/components/procedure/chorus_form_component/chorus_form_component.html.haml +++ b/app/components/procedure/chorus_form_component/chorus_form_component.html.haml @@ -1,12 +1,10 @@ -= form_for([procedure, @chorus_configuration],url: admin_procedure_chorus_path(procedure), method: :put) do |f| += form_for([procedure, @chorus_configuration], url: admin_procedure_chorus_path(procedure), method: :put) do |f| - map_attribute_to_autocomplete_endpoint.map do |chorus_configuration_attribute, datasource_endpoint| - label_id = "#{chorus_configuration_attribute}-label" .fr-select-group - = f.label chorus_configuration_attribute, class: 'fr-label', id: label_id - = render Dsfr::ComboboxComponent.new form: f, - url: datasource_endpoint, - selected: format_displayed_value(chorus_configuration_attribute), - input_html_options: { id: chorus_configuration_attribute, class: 'fr-select', describedby: label_id, name: :chorus_configuration_attribute } do - = f.hidden_field chorus_configuration_attribute, data: { value_slot: 'data' }, value: format_hidden_value(chorus_configuration_attribute) + = f.label chorus_configuration_attribute, class: 'fr-label', id: label_id, for: chorus_configuration_attribute + %react-fragment + = render ReactComponent.new "ComboBox/RemoteComboBox", **react_props(f.field_name(:chorus_configuration_attribute), chorus_configuration_attribute, datasource_endpoint) do + = render ReactComponent.new "ComboBox/ComboBoxValueSlot", field: :data, name: f.field_name(chorus_configuration_attribute) = f.submit "Enregister", class: 'fr-btn' From 8d6dc625f30ad73f323f46ea08c371d013aca847 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 7 May 2024 11:31:27 +0200 Subject: [PATCH 16/39] refactor(dossier): instructeur filter --- .../dossiers/instructeur_filter_component.rb | 16 ++++++++++++---- .../instructeur_filter_component.html.haml | 7 ++----- .../instructeurs/procedures_controller.rb | 5 ++--- app/views/instructeurs/procedures/show.html.haml | 10 ++-------- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/app/components/dossiers/instructeur_filter_component.rb b/app/components/dossiers/instructeur_filter_component.rb index 5f55441b1..7e841f5c4 100644 --- a/app/components/dossiers/instructeur_filter_component.rb +++ b/app/components/dossiers/instructeur_filter_component.rb @@ -8,10 +8,6 @@ class Dossiers::InstructeurFilterComponent < ApplicationComponent attr_reader :procedure, :procedure_presentation, :statut, :field_id - def filterable_fields_for_select - procedure_presentation.filterable_fields_options - end - def field_type return :text if field_id.nil? procedure_presentation.field_type(field_id) @@ -20,4 +16,16 @@ class Dossiers::InstructeurFilterComponent < ApplicationComponent def options_for_select_of_field procedure_presentation.field_enum(field_id) end + + def filter_react_props + { + selected_key: @field_id || '', + items: procedure_presentation.filterable_fields_options, + name: :field, + id: 'search-filter', + 'aria-describedby': 'instructeur-filter-combo-label', + form: 'filter-component', + data: { no_autosubmit: 'input blur', no_autosubmit_on_empty: 'true', autosubmit_target: 'input' } + } + end end diff --git a/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml b/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml index 8afad1b45..f98e93700 100644 --- a/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml +++ b/app/components/dossiers/instructeur_filter_component/instructeur_filter_component.html.haml @@ -1,11 +1,8 @@ = form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { turbo: true, controller: 'autosubmit' } do .fr-select-group = label_tag :field, t('.column'), class: 'fr-label fr-m-0', id: 'instructeur-filter-combo-label', for: 'search-filter' - = render Dsfr::ComboboxComponent.new form: nil, - options: filterable_fields_for_select, - selected: field_id, - input_html_options: { name: :field, id: 'search-filter', class: 'fr-select', describedby: 'instructeur-filter-combo-label', allows_custom_value: false, form_id: 'filter-component' }, - hidden_html_options: { data: { no_autosubmit: ['input', 'blur'].join(' '), no_autosubmit_on_empty: "true", autosubmit_target: 'input' } } + %react-fragment + = render ReactComponent.new "ComboBox/SingleComboBox", **filter_react_props %input.hidden{ type: 'submit', formaction: update_filter_instructeur_procedure_path(procedure), data: { autosubmit_target: 'submitter' } } diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 2b8702cf3..f737ea591 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -73,7 +73,6 @@ module Instructeurs @current_filters = current_filters @displayable_fields_for_select, @displayable_fields_selected = procedure_presentation.displayable_fields_for_select - @filterable_fields_for_select = procedure_presentation.filterable_fields_options @counts = current_instructeur .dossiers_count_summary(groupe_instructeur_ids) .symbolize_keys @@ -135,8 +134,8 @@ module Instructeurs end def update_displayed_fields - values = params['values'].presence || [].to_json - procedure_presentation.update_displayed_fields(JSON.parse(values)) + values = params['values'].presence || [] + procedure_presentation.update_displayed_fields(values) redirect_back(fallback_location: instructeur_procedure_url(procedure)) end diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index e3ec7a5a0..a5bd8de97 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -110,14 +110,8 @@ = t('views.instructeurs.dossiers.personalize') - menu.with_form do = form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do - = hidden_field_tag :values, nil - = react_component("ComboMultiple", - options: @displayable_fields_for_select, - selected: @displayable_fields_selected, - disabled: [], - label: 'Colonne à afficher', - group: '.columns-form', - name: 'values') + %react-fragment + = render ReactComponent.new "ComboBox/MultiComboBox", items: @displayable_fields_for_select, selected_keys: @displayable_fields_selected, name: 'values[]', 'aria-label': 'Colonne à afficher' = submit_tag t('views.instructeurs.dossiers.save'), class: 'fr-btn fr-btn--secondary' From 9f9243499c9eadaa503251e747aebc1b816ef3a4 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 7 May 2024 21:37:54 +0200 Subject: [PATCH 17/39] refactor(combobox): fix specs --- .../experts_procedures_controller_spec.rb | 4 +- .../groupe_instructeurs_controller_spec.rb | 10 +- .../procedures_controller_spec.rb | 2 +- .../experts/avis_controller_spec.rb | 8 +- .../instructeurs/dossiers_controller_spec.rb | 12 +-- .../champs/annuaire_education_champ_spec.rb | 16 ++-- spec/support/system_helpers.rb | 23 +---- .../administrateurs/procedure_update_spec.rb | 2 +- spec/system/instructeurs/expert_spec.rb | 5 +- spec/system/instructeurs/instruction_spec.rb | 13 +-- .../instructeurs/procedure_filters_spec.rb | 92 ++++++------------- spec/system/users/brouillon_spec.rb | 4 +- .../_envoyer_dossier_block.html.haml_spec.rb | 2 +- 13 files changed, 70 insertions(+), 123 deletions(-) diff --git a/spec/controllers/administrateurs/experts_procedures_controller_spec.rb b/spec/controllers/administrateurs/experts_procedures_controller_spec.rb index 7c99bbbc6..1807a0cd5 100644 --- a/spec/controllers/administrateurs/experts_procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/experts_procedures_controller_spec.rb @@ -26,7 +26,7 @@ describe Administrateurs::ExpertsProceduresController, type: :controller do subject { post :create, params: params } context 'when inviting multiple valid experts' do - let(:params) { { procedure_id: procedure.id, emails: [expert.email, "new@expert.fr"].to_json } } + let(:params) { { procedure_id: procedure.id, emails: [expert.email, "new@expert.fr"] } } it 'creates experts' do subject @@ -38,7 +38,7 @@ describe Administrateurs::ExpertsProceduresController, type: :controller do end context 'when inviting expert using an email with typos' do - let(:params) { { procedure_id: procedure.id, emails: ['martin@oraneg.fr'].to_json } } + let(:params) { { procedure_id: procedure.id, emails: ['martin@oraneg.fr'] } } render_views it 'warns' do subject diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index 7727cbfa0..4b5c3d796 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -332,14 +332,14 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do describe '#add_instructeur_procedure_non_routee' do # faire la meme chose sur une procedure non routee let(:procedure_non_routee) { create(:procedure, administrateur: admin) } - let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json } + let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'] } let(:manager) { false } before { procedure_non_routee.administrateurs_procedures.where(administrateur: admin).update_all(manager:) } subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure_non_routee.id, id: procedure_non_routee.defaut_groupe_instructeur.id } } context 'when all emails are valid' do - let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json } + let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'] } it do expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure_non_routee) expect(subject.request.flash[:alert]).to be_nil @@ -348,7 +348,7 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do end context 'when there is at least one bad email' do - let(:emails) { ['badmail', 'instructeur2@gmail.com'].to_json } + let(:emails) { ['badmail', 'instructeur2@gmail.com'] } it do expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure_non_routee) expect(subject.request.flash[:alert]).to be_present @@ -359,7 +359,7 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do context 'when the admin wants to assign an instructor who is already assigned on this procedure' do let(:instructeur) { create(:instructeur) } before { procedure_non_routee.groupe_instructeurs.first.add_instructeurs(emails: [instructeur.user.email]) } - let(:emails) { [instructeur.email].to_json } + let(:emails) { [instructeur.email] } it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure_non_routee) } end @@ -376,7 +376,7 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do params: { procedure_id: procedure.id, id: gi_1_2.id, - emails: new_instructeur_emails.to_json + emails: new_instructeur_emails } end diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb index d5198d952..3259d3d0b 100644 --- a/spec/controllers/administrateurs/procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/procedures_controller_spec.rb @@ -13,7 +13,7 @@ describe Administrateurs::ProceduresController, type: :controller do let(:lien_site_web) { 'http://mon-site.gouv.fr' } let(:zone) { create(:zone) } let(:zone_ids) { [zone.id] } - let(:tags) { "[\"planete\",\"environnement\"]" } + let(:tags) { ["planete", "environnement"] } describe '#apercu' do subject { get :apercu, params: { id: procedure.id } } diff --git a/spec/controllers/experts/avis_controller_spec.rb b/spec/controllers/experts/avis_controller_spec.rb index e58b4d2bb..56865796e 100644 --- a/spec/controllers/experts/avis_controller_spec.rb +++ b/spec/controllers/experts/avis_controller_spec.rb @@ -367,7 +367,7 @@ describe Experts::AvisController, type: :controller do let(:previous_avis_confidentiel) { false } let(:previous_revoked_at) { nil } let!(:previous_avis) { create(:avis, dossier:, claimant:, experts_procedure:, confidentiel: previous_avis_confidentiel, revoked_at: previous_revoked_at) } - let(:emails) { '["a@b.com"]' } + let(:emails) { ["a@b.com"] } let(:introduction) { 'introduction' } let(:created_avis) { Avis.last } let!(:old_avis_count) { Avis.count } @@ -394,7 +394,7 @@ describe Experts::AvisController, type: :controller do end context 'when an invalid email' do - let(:emails) { "[\"toto.fr\"]" } + let(:emails) { ["toto.fr"] } it do expect(response).to render_template :instruction @@ -414,7 +414,7 @@ describe Experts::AvisController, type: :controller do end context 'ask review with attachment' do - let(:emails) { "[\"toto@totomail.com\"]" } + let(:emails) { ["toto@totomail.com"] } it do expect(created_avis.introduction_file).to be_attached @@ -425,7 +425,7 @@ describe Experts::AvisController, type: :controller do end context 'with multiple emails' do - let(:emails) { "[\"toto.fr\",\"titi@titimail.com\"]" } + let(:emails) { ["toto.fr", "titi@titimail.com"] } it do expect(response).to render_template :instruction diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 47cadd89c..198c75a13 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -28,7 +28,7 @@ describe Instructeurs::DossiersController, type: :controller do post( :send_to_instructeurs, params: { - recipients: [recipient.id].to_json, + recipients: [recipient.id], procedure_id: procedure.id, dossier_id: dossier.id } @@ -776,7 +776,7 @@ describe Instructeurs::DossiersController, type: :controller do } end - let(:emails) { "[\"email@a.com\"]" } + let(:emails) { ["email@a.com"] } context "notifications updates" do context 'when an instructeur follows the dossier' do @@ -811,7 +811,7 @@ describe Instructeurs::DossiersController, type: :controller do it { expect(response).to redirect_to(avis_instructeur_dossier_path(dossier.procedure, dossier)) } context "with an invalid email" do - let(:emails) { "[\"emaila.com\"]" } + let(:emails) { ["emaila.com"] } before { subject } @@ -822,7 +822,7 @@ describe Instructeurs::DossiersController, type: :controller do end context "with no email" do - let(:emails) { "" } + let(:emails) { [] } before { subject } @@ -833,7 +833,7 @@ describe Instructeurs::DossiersController, type: :controller do end context 'with multiple emails' do - let(:emails) { "[\"toto.fr\",\"titi@titimail.com\"]" } + let(:emails) { ["toto.fr", "titi@titimail.com"] } before { subject } @@ -845,7 +845,7 @@ describe Instructeurs::DossiersController, type: :controller do end context 'when the expert do not want to receive notification' do - let(:emails) { "[\"email@a.com\"]" } + let(:emails) { ["email@a.com"] } let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: dossier.procedure, notify_on_new_avis: false) } before { subject } diff --git a/spec/models/champs/annuaire_education_champ_spec.rb b/spec/models/champs/annuaire_education_champ_spec.rb index 8c8f58be5..3ac3a87a0 100644 --- a/spec/models/champs/annuaire_education_champ_spec.rb +++ b/spec/models/champs/annuaire_education_champ_spec.rb @@ -22,21 +22,19 @@ RSpec.describe Champs::AnnuaireEducationChamp do it_behaves_like "a data updater (without updating the value)", '' end - context 'when data is inconsistent' do - let(:data) { { 'yo' => 'lo' } } - it_behaves_like "a data updater (without updating the value)", { 'yo' => 'lo' } - end - context 'when data is consistent' do let(:data) { { - 'nom_etablissement': "karrigel an ankou", + 'nom_etablissement' => "karrigel an ankou", 'nom_commune' => 'kumun', 'identifiant_de_l_etablissement' => '666667' - }.with_indifferent_access + } + } + it_behaves_like "a data updater (without updating the value)", { + 'nom_etablissement' => "karrigel an ankou", + 'nom_commune' => 'kumun', + 'identifiant_de_l_etablissement' => '666667' } - it { expect { subject }.to change { champ.reload.data }.to(data) } - it { expect { subject }.to change { champ.reload.value }.to('karrigel an ankou, kumun (666667)') } end end end diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb index 6a5d6fad7..b725cd798 100644 --- a/spec/support/system_helpers.rb +++ b/spec/support/system_helpers.rb @@ -106,25 +106,10 @@ module SystemHelpers end end - def select_combobox(libelle, fill_with, value, check: true) - fill_in libelle, with: fill_with - find('li[role="option"][data-reach-combobox-option]', text: value, wait: 5).click - if check - check_selected_value(libelle, with: value) - end - end - - def check_selected_value(libelle, with:) - field = find_hidden_field_for(libelle) - value = field.value.starts_with?('[') ? JSON.parse(field.value) : field.value - if value.is_a?(Array) - if with.is_a?(Array) - expect(value.sort).to eq(with.sort) - else - expect(value).to include(with) - end - else - expect(value).to eq(with) + def select_combobox(libelle, value, custom_value: false) + fill_in libelle, with: custom_value ? "#{value}," : value + if !custom_value + find_field(libelle).send_keys(:down, :enter) end end diff --git a/spec/system/administrateurs/procedure_update_spec.rb b/spec/system/administrateurs/procedure_update_spec.rb index 65440e57e..610c37933 100644 --- a/spec/system/administrateurs/procedure_update_spec.rb +++ b/spec/system/administrateurs/procedure_update_spec.rb @@ -60,7 +60,7 @@ describe 'Administrateurs can edit procedures', js: true do procedure.update!(tags: ['social']) visit edit_admin_procedure_path(procedure) - select_combobox('procedure_tags_combo', 'planete', 'planete', check: false) + select_combobox('procedure_tags_combo', 'planete', custom_value: true) click_on 'Enregistrer' expect(procedure.reload.tags).to eq(['social', 'planete']) diff --git a/spec/system/instructeurs/expert_spec.rb b/spec/system/instructeurs/expert_spec.rb index 64a66c1e9..426cc9655 100644 --- a/spec/system/instructeurs/expert_spec.rb +++ b/spec/system/instructeurs/expert_spec.rb @@ -29,7 +29,8 @@ describe 'Inviting an expert:', js: true do within('.fr-sidemenu') { click_on 'Demander un avis' } expect(page).to have_current_path(avis_new_instructeur_dossier_path(procedure, dossier)) - page.execute_script("document.querySelector('#avis_emails').value = '[\"#{expert.email}\",\"#{expert2.email}\"]'") + fill_in 'Emails', with: "#{expert.email}," + fill_in 'Emails', with: expert2.email fill_in 'avis_introduction', with: 'Bonjour, merci de me donner votre avis sur ce dossier.' check 'avis_invite_linked_dossiers' page.select 'confidentiel', from: 'avis_confidentiel' @@ -109,7 +110,7 @@ describe 'Inviting an expert:', js: true do within('.fr-sidemenu') { click_on 'Demander un avis' } expect(page).to have_current_path(avis_new_instructeur_dossier_path(procedure, dossier)) - fill_in 'Emails', with: "#{expert.email}; #{expert2.email}" + select_combobox 'Emails', expert.email fill_in 'avis_introduction', with: 'Bonjour, merci de me donner votre avis sur ce dossier.' check 'avis_invite_linked_dossiers' page.select 'confidentiel', from: 'avis_confidentiel' diff --git a/spec/system/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb index d6230e9b5..f36b15f5f 100644 --- a/spec/system/instructeurs/instruction_spec.rb +++ b/spec/system/instructeurs/instruction_spec.rb @@ -160,13 +160,10 @@ describe 'Instructing a dossier:', js: true do within('.fr-sidemenu') { click_on 'Demander un avis' } expect(page).to have_current_path(avis_new_instructeur_dossier_path(procedure, dossier)) - expert_email_formated = "[\"expert@tps.com\"]" expert_email = 'expert@tps.com' - ask_confidential_avis(expert_email_formated, 'a good introduction') + ask_confidential_avis(expert_email, 'a good introduction') - expert_email_formated = "[\"#{instructeur2.email}\"]" - expert_email = instructeur2.email - ask_confidential_avis(expert_email_formated, 'a good introduction') + ask_confidential_avis(instructeur2.email, 'a good introduction') click_on 'Personnes impliquées' expect(page).to have_text(expert_email) @@ -189,8 +186,8 @@ describe 'Instructing a dossier:', js: true do click_on 'Personnes impliquées' - select_combobox('Emails', instructeur_2.email, instructeur_2.email, check: false) - select_combobox('Emails', instructeur_3.email, instructeur_3.email, check: false) + select_combobox('Emails', instructeur_2.email) + select_combobox('Emails', instructeur_3.email) click_on 'Envoyer' @@ -287,7 +284,7 @@ describe 'Instructing a dossier:', js: true do end def ask_confidential_avis(to, introduction) - page.execute_script("document.querySelector('#avis_emails').value = '#{to}'") + fill_in 'avis_emails', with: to fill_in 'avis_introduction', with: introduction select 'confidentiel', from: 'avis_confidentiel' within('form#new_avis') { click_on 'Demander un avis' } diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb index 0e3a6db03..3900fe426 100644 --- a/spec/system/instructeurs/procedure_filters_spec.rb +++ b/spec/system/instructeurs/procedure_filters_spec.rb @@ -88,33 +88,13 @@ describe "procedure filters" do scenario "should be able to user custom fiters", js: true do # use date filter - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: "En construction le", wait: 5).click - find("input#value[type=date]", visible: true) - fill_in "Valeur", with: "10/10/2010" - click_button "Ajouter le filtre" - expect(page).to have_no_css("#search-filter", visible: true) + add_filter("En construction le", "10/10/2010", type: :date) # use statut dropdown filter - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: "Statut", wait: 5).click - find("select#value", visible: false) - select 'En construction', from: "Valeur" - click_button "Ajouter le filtre" - expect(page).to have_no_css("#search-filter", visible: true) + add_filter('Statut', 'En construction', type: :enum) # use choice dropdown filter - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: "Choix unique", wait: 5).click - find("select#value", visible: false) - select 'val1', from: "Valeur" - click_button "Ajouter le filtre" + add_filter('Choix unique', 'val1', type: :enum) end describe 'with a vcr cached cassette' do @@ -124,14 +104,7 @@ describe "procedure filters" do departement_champ.reload champ_select_value = "#{departement_champ.external_id} – #{departement_champ.value}" - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: departement_champ.libelle, wait: 5).click - find("select#value", visible: true) - select champ_select_value, from: "Valeur" - click_button "Ajouter le filtre" - find("select#value", visible: false) # w8 for filter to be applied + add_filter(departement_champ.libelle, champ_select_value, type: :enum) expect(page).to have_link(new_unfollow_dossier.id.to_s) end @@ -140,14 +113,7 @@ describe "procedure filters" do region_champ.update!(value: 'Bretagne', external_id: '53') region_champ.reload - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: region_champ.libelle, wait: 5).click - find("select#value", visible: true) - select region_champ.value, from: "Valeur" - click_button "Ajouter le filtre" - find("select#value", visible: false) # w8 for filter to be applied + add_filter(region_champ.libelle, region_champ.value, type: :enum) expect(page).to have_link(new_unfollow_dossier.id.to_s) end end @@ -155,7 +121,7 @@ describe "procedure filters" do scenario "should be able to add and remove two filters for the same field", js: true do add_filter(type_de_champ.libelle, champ.value) add_filter(type_de_champ.libelle, champ_2.value) - add_enum_filter('Groupe instructeur', procedure.groupe_instructeurs.first.label) + add_filter('Groupe instructeur', procedure.groupe_instructeurs.first.label, type: :enum) within ".dossiers-table" do expect(page).to have_link(new_unfollow_dossier.id.to_s, exact: true) @@ -185,40 +151,40 @@ describe "procedure filters" do end end + def add_filter(column_name, filter_value, type: :text) + click_on 'Sélectionner un filtre' + wait_until { all("#search-filter").size == 1 } + find('#search-filter + button', wait: 5).click + find('.fr-menu__item', text: column_name, wait: 5).click + case type + when :text + fill_in "Valeur", with: filter_value + when :date + find("input#value[type=date]", visible: true) + fill_in "Valeur", with: filter_value + when :enum + find("select#value", visible: false) + select filter_value, from: "Valeur" + end + click_button "Ajouter le filtre" + expect(page).to have_no_css("#search-filter", visible: true) + end + def remove_filter(filter_value) click_link text: filter_value end - def add_filter(column_name, filter_value) - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: column_name, wait: 5).click - fill_in "Valeur", with: filter_value - click_button "Ajouter le filtre" - expect(page).to have_no_css("#search-filter", visible: true) - end - - def add_enum_filter(column_name, filter_value) - click_on 'Sélectionner un filtre' - wait_until { all("#search-filter").size == 1 } - find('#search-filter', wait: 5).click - find('.fr-menu__item', text: column_name, wait: 5).click - select filter_value, from: "Valeur" - click_button "Ajouter le filtre" - expect(page).to have_no_css("#search-filter", visible: true) - end - def add_column(column_name) click_on 'Personnaliser' - select_combobox('Colonne à afficher', column_name, column_name, check: false) + select_combobox('Colonne à afficher', column_name) click_button "Enregistrer" end def remove_column(column_name) click_on 'Personnaliser' - click_button column_name - find("body").native.send_key("Escape") + within '.fr-tag-list' do + find('.fr-tag', text: column_name).find('button').click + end click_button "Enregistrer" end end diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 6f5a65135..1ba5bfa92 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -38,11 +38,11 @@ describe 'The user' do select('02 – Aisne', from: form_id_for('departements')) fill_in('communes', with: '60400') - find('li', text: 'Brétigny (60400)').click + find('.fr-menu__item', text: 'Brétigny (60400)').click wait_until { champ_value_for('communes') == "Brétigny" } fill_in('address', with: '78 Rue du Grés 30310 Vergè') - find('li', text: '78 Rue du Grés 30310 Vergèze').click + find('.fr-menu__item', text: '78 Rue du Grés 30310 Vergèze').click wait_until { champ_value_for('address') == '78 Rue du Grés 30310 Vergèze' } wait_until { champ_for('address').full_address? } expect(champ_for('address').departement_code_and_name).to eq('30 – Gard') diff --git a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb index 1c3107fbb..94b8d0e57 100644 --- a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb @@ -13,7 +13,7 @@ describe 'instructeurs/dossiers/envoyer_dossier_block', type: :view do let(:instructeur) { create(:instructeur, email: 'yop@totomail.fr') } let(:potential_recipients) { [instructeur] } - it { is_expected.to match(/data-react-props.*#{instructeur.email}/) } + it { is_expected.to match(/props.*#{instructeur.email}/) } it { is_expected.to have_css(".fr-btn") } end From c17351e50a5ca374ad42a4389ab1c24caf166d66 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 15 May 2024 23:16:55 +0200 Subject: [PATCH 18/39] refactor(combo): use new multicombobox --- .../experts_procedures_controller.rb | 4 ++-- .../groupe_instructeurs_controller.rb | 4 ++-- .../administrateurs/procedures_controller.rb | 6 +----- .../concerns/create_avis_concern.rb | 8 +++---- ...gestionnaire_administrateurs_controller.rb | 4 ++-- ...e_gestionnaire_gestionnaires_controller.rb | 4 ++-- .../instructeurs/dossiers_controller.rb | 4 ++-- .../groupe_gestionnaires_controller.rb | 4 ++-- .../manager/procedures_controller.rb | 5 ++--- .../experts_procedures/index.html.haml | 16 +++++++------- .../_instructeurs.html.haml | 10 ++------- .../procedures/_informations.html.haml | 21 +++++++++---------- .../dossiers/_envoyer_dossier_block.html.haml | 9 ++------ app/views/manager/procedures/show.html.erb | 19 ++++++++--------- app/views/shared/avis/_form.html.haml | 11 ++-------- 15 files changed, 51 insertions(+), 78 deletions(-) diff --git a/app/controllers/administrateurs/experts_procedures_controller.rb b/app/controllers/administrateurs/experts_procedures_controller.rb index 64abc5c38..c9e3c5763 100644 --- a/app/controllers/administrateurs/experts_procedures_controller.rb +++ b/app/controllers/administrateurs/experts_procedures_controller.rb @@ -9,8 +9,8 @@ module Administrateurs end def create - emails = params['emails'].presence || [].to_json - emails = JSON.parse(emails).map { EmailSanitizer.sanitize(_1) } + emails = params['emails'].presence || [] + emails = emails.map { EmailSanitizer.sanitize(_1) } @maybe_typos, no_suggestions = emails .map { |email| [email, EmailChecker.check(email:)[:suggestions]&.first] } .partition { _1[1].present? } diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index 50a940690..4e566982b 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -218,8 +218,8 @@ module Administrateurs end def add_instructeur - emails = params['emails'].presence || [].to_json - emails = JSON.parse(emails).map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } + emails = params[:emails].presence || [] + emails = emails.map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } instructeurs, invalid_emails = groupe_instructeur.add_instructeurs(emails:) diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 874290617..98a3073e0 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -527,11 +527,10 @@ module Administrateurs :accuse_lecture, :api_entreprise_token, :duree_conservation_dossiers_dans_ds, - { zone_ids: [] }, :lien_dpo, :opendata, :procedure_expires_when_termine_enabled, - :tags + { zone_ids: [], tags: [] } ] editable_params << :piece_justificative_multiple if @procedure && !@procedure.piece_justificative_multiple? @@ -544,9 +543,6 @@ module Administrateurs if permited_params[:auto_archive_on].present? permited_params[:auto_archive_on] = Date.parse(permited_params[:auto_archive_on]) + 1.day end - if permited_params[:tags].present? - permited_params[:tags] = JSON.parse(permited_params[:tags]) - end permited_params end diff --git a/app/controllers/concerns/create_avis_concern.rb b/app/controllers/concerns/create_avis_concern.rb index f7b821a1f..ad29b06c0 100644 --- a/app/controllers/concerns/create_avis_concern.rb +++ b/app/controllers/concerns/create_avis_concern.rb @@ -4,7 +4,7 @@ module CreateAvisConcern private def create_avis_from_params(dossier, instructeur_or_expert, confidentiel = false) - if create_avis_params[:emails].empty? + if create_avis_params[:emails].blank? avis = Avis.new(create_avis_params) errors = avis.errors errors.add(:emails, :blank) @@ -19,8 +19,8 @@ module CreateAvisConcern # the :emails parameter is a 1-element array. # Hence the call to first # https://github.com/rails/rails/issues/17225 - expert_emails = create_avis_params[:emails].presence || [].to_json - expert_emails = JSON.parse(expert_emails).map(&:strip).map(&:downcase) + expert_emails = create_avis_params[:emails].presence || [] + expert_emails = expert_emails.map(&:strip).map(&:downcase) allowed_dossiers = [dossier] if create_avis_params[:invite_linked_dossiers].present? @@ -84,6 +84,6 @@ module CreateAvisConcern end def create_avis_params - params.require(:avis).permit(:introduction_file, :introduction, :confidentiel, :invite_linked_dossiers, :emails, :question_label) + params.require(:avis).permit(:introduction_file, :introduction, :confidentiel, :invite_linked_dossiers, :question_label, emails: []) end end diff --git a/app/controllers/gestionnaires/groupe_gestionnaire_administrateurs_controller.rb b/app/controllers/gestionnaires/groupe_gestionnaire_administrateurs_controller.rb index da05c8a4b..8b3843167 100644 --- a/app/controllers/gestionnaires/groupe_gestionnaire_administrateurs_controller.rb +++ b/app/controllers/gestionnaires/groupe_gestionnaire_administrateurs_controller.rb @@ -6,8 +6,8 @@ module Gestionnaires end def create - emails = [params.require(:administrateur)[:email]].to_json - emails = JSON.parse(emails).map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } + emails = [params.require(:administrateur)[:email]].compact + emails = emails.map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } administrateurs_to_add, valid_emails, invalid_emails = Administrateur.find_all_by_identifier_with_emails(emails:) not_found_emails = valid_emails - administrateurs_to_add.map(&:email) diff --git a/app/controllers/gestionnaires/groupe_gestionnaire_gestionnaires_controller.rb b/app/controllers/gestionnaires/groupe_gestionnaire_gestionnaires_controller.rb index bf7ba9fb9..c098d2d80 100644 --- a/app/controllers/gestionnaires/groupe_gestionnaire_gestionnaires_controller.rb +++ b/app/controllers/gestionnaires/groupe_gestionnaire_gestionnaires_controller.rb @@ -6,8 +6,8 @@ module Gestionnaires end def create - emails = [params.require(:gestionnaire)[:email]].to_json - emails = JSON.parse(emails).map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } + emails = [params.require(:gestionnaire)[:email]].compact + emails = emails.map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } gestionnaires_to_add, valid_emails, invalid_emails = Gestionnaire.find_all_by_identifier_with_emails(emails:) not_found_emails = valid_emails - gestionnaires_to_add.map(&:email) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 884309712..1605c4de1 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -86,9 +86,9 @@ module Instructeurs end def send_to_instructeurs - recipients = params['recipients'].presence || [].to_json + recipients = params['recipients'].presence || [] # instructeurs are scoped by groupe_instructeur to avoid enumeration - recipients = dossier.groupe_instructeur.instructeurs.where(id: JSON.parse(recipients)) + recipients = dossier.groupe_instructeur.instructeurs.where(id: recipients) if recipients.present? recipients.each do |recipient| diff --git a/app/controllers/manager/groupe_gestionnaires_controller.rb b/app/controllers/manager/groupe_gestionnaires_controller.rb index 66bdc82db..f009ae072 100644 --- a/app/controllers/manager/groupe_gestionnaires_controller.rb +++ b/app/controllers/manager/groupe_gestionnaires_controller.rb @@ -2,8 +2,8 @@ module Manager class GroupeGestionnairesController < Manager::ApplicationController def add_gestionnaire groupe_gestionnaire = GroupeGestionnaire.find(params[:id]) - emails = [params['emails'].presence || ''].to_json - emails = JSON.parse(emails).map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } + emails = [params['emails']].compact + emails = emails.map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } gestionnaires_to_add, valid_emails, invalid_emails = Gestionnaire.find_all_by_identifier_with_emails(emails:) not_found_emails = valid_emails - gestionnaires_to_add.map(&:email) diff --git a/app/controllers/manager/procedures_controller.rb b/app/controllers/manager/procedures_controller.rb index 6fb14e6d2..92d5e321a 100644 --- a/app/controllers/manager/procedures_controller.rb +++ b/app/controllers/manager/procedures_controller.rb @@ -111,8 +111,7 @@ module Manager end def add_tags - tags_h = { tags: JSON.parse(tags_params[:tags]) } - if procedure.update(tags_h) + if procedure.update(tags: tags_params[:tags]) flash.notice = "Le modèle est mis à jour." else flash.alert = procedure.errors.full_messages.join(', ') @@ -181,7 +180,7 @@ module Manager end def tags_params - params.require(:procedure).permit(:tags) + params.require(:procedure).permit(tags: []) end def template_params diff --git a/app/views/administrateurs/experts_procedures/index.html.haml b/app/views/administrateurs/experts_procedures/index.html.haml index bffd9f792..865d1cb67 100644 --- a/app/views/administrateurs/experts_procedures/index.html.haml +++ b/app/views/administrateurs/experts_procedures/index.html.haml @@ -65,15 +65,13 @@ .instructeur-wrapper %p#experts-emails Entrez les adresses emails des experts que vous souhaitez ajouter à la liste prédéfinie - = hidden_field_tag :emails, nil - = react_component("ComboMultiple", - options: [], - selected: [], disabled: [], - group: '.instructeur-wrapper', - name: 'emails', - label: 'Emails', - describedby: 'experts-emails', - acceptNewValues: true) + %react-fragment + = render ReactComponent.new "ComboBox/MultiComboBox", + id: 'emails', + name: 'emails[]', + allows_custom_value: true, + 'aria-label': 'Emails', + 'aria-describedby': 'experts-emails' = f.submit 'Ajouter à la liste', class: 'fr-btn' diff --git a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml index 3e8dd7643..d4ff19e32 100644 --- a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml @@ -9,14 +9,8 @@ - if disabled_as_super_admin = f.select :emails, available_instructeur_emails, {}, disabled: disabled_as_super_admin, id: 'instructeur_emails' - else - = hidden_field_tag :emails, nil - = react_component("ComboMultiple", - options: available_instructeur_emails, selected: [], disabled: [], - group: '.instructeur-wrapper', - id: 'instructeur_emails', - name: 'emails', - label: 'Emails', - acceptNewValues: true) + %react-fragment + = render ReactComponent.new 'ComboBox/MultiComboBox', items: available_instructeur_emails, id: 'instructeur_emails', name: 'emails[]', allows_custom_value: true, 'aria-label': 'Emails' = f.submit 'Affecter', class: 'fr-btn', disabled: disabled_as_super_admin diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml index a3d361a52..98458512c 100644 --- a/app/views/administrateurs/procedures/_informations.html.haml +++ b/app/views/administrateurs/procedures/_informations.html.haml @@ -122,17 +122,16 @@ .fr-fieldset__element = f.label :tags, 'Associez des thématiques à la démarche', class: 'fr-label' %p.fr-hint-text Par des mots ou des expressions que vous attribuez aux démarches pour décrire leur contenu et pour les retrouver. Les tags sont partagés avec la communauté, ce qui vous permet de voir les tags attribués aux démarches créées par les autres administrateurs. - = hidden_field_tag 'procedure[tags]', JSON.generate(@procedure.tags) - = react_component("ComboMultiple", - id: "procedure_tags_combo", - options: Procedure.tags, - selected: @procedure.tags, - disabled: [], - label: 'Tags', - group: '.procedure_tags_combo', - name: 'tags', - describedby: 'procedure-tags', - acceptNewValues: true) + %react-fragment + = render ReactComponent.new "ComboBox/MultiComboBox", + id: "procedure_tags_combo", + items: Procedure.tags, + selected_keys: @procedure.tags, + name: 'procedure[tags][]', + value_separator: ',|;', + allows_custom_value: true, + 'aria-label': 'Tags', + 'aria-describedby': 'procedure-tags' %details.procedure-form__options-details %summary.procedure-form__options-summary diff --git a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml index b60bf1c32..9c5a6bb78 100644 --- a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml +++ b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml @@ -7,12 +7,7 @@ %p.tab-paragrah.mb-1 Le destinataire suivra automatiquement le dossier = form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form fr-mb-4w' } do |f| - = hidden_field_tag :recipients, nil - = react_component("ComboMultiple", - options: potential_recipients.map{|r| [r.email, r.id]}, - selected: [], disabled: [], - group: '.recipients-form', - name: 'recipients', - label: 'Emails') + %react-fragment + = render ReactComponent.new "ComboBox/MultiComboBox", items: potential_recipients.map { [_1.email, _1.id] }, name: 'recipients[]', 'aria-label': 'Emails' = f.submit "Envoyer", class: "fr-btn fr-mt-2w" diff --git a/app/views/manager/procedures/show.html.erb b/app/views/manager/procedures/show.html.erb index 38e6919e9..17f815bdd 100644 --- a/app/views/manager/procedures/show.html.erb +++ b/app/views/manager/procedures/show.html.erb @@ -93,16 +93,15 @@ as well as a link to its edit page. <% elsif attribute.name == 'tags' %> <%= form_for procedure, url: add_tags_manager_procedure_path(procedure), html: { class: 'form procedure-form__column--form fr-background-alt--blue-france mt-1' } do %> - <%= hidden_field_tag 'procedure[tags]', nil %> - <%= react_component("ComboMultiple", - options: Procedure.tags, - selected: procedure.tags, - disabled: [], - label: 'Tags', - group: '.procedure-form__column--form', - name: 'tags', - describedby: 'procedure-tags', - acceptNewValues: true) %> + + <%= render ReactComponent.new "ComboBox/MultiComboBox", + items: Procedure.tags, + selected_keys: procedure.tags, + value_separator: ',|;', + allows_custom_value: true, + name: 'procedure[tags][]', + 'aria-label': 'Tags' %> + <% end %> diff --git a/app/views/shared/avis/_form.html.haml b/app/views/shared/avis/_form.html.haml index 43f3dad28..2b5e00b05 100644 --- a/app/views/shared/avis/_form.html.haml +++ b/app/views/shared/avis/_form.html.haml @@ -10,16 +10,9 @@ = render NestedForms::FormOwnerComponent.new = form_for avis, url: url, html: { multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f| - = hidden_field_tag 'avis[emails]', nil .fr-input-group - = react_component("ComboMultiple", - options: current_expert_not_instructeur? ? [] : @experts_emails, - selected: [], disabled: [], - label: 'Emails', - group: '.ask-avis', - name: 'emails', - describedby: 'avis-emails-description', - acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation) + %react-fragment + = render ReactComponent.new "ComboBox/MultiComboBox", items: current_expert_not_instructeur? ? [] : @experts_emails, name: f.field_name(:emails, multiple: true), id: 'avis_emails', 'aria-label': 'Emails', 'aria-describedby': 'avis-emails-description', allows_custom_value: !@dossier.procedure.experts_require_administrateur_invitation .fr-input-group = f.label :introduction, t('helpers.label.introduction'), class: 'fr-label' From e2ba14583ce2a18cb64833f325365fbbb3060fb6 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 15 May 2024 23:17:24 +0200 Subject: [PATCH 19/39] chore(combobox): remove old styles --- app/assets/stylesheets/carte.scss | 4 - app/assets/stylesheets/dsfr.scss | 9 ++ app/assets/stylesheets/forms.scss | 120 +----------------- app/assets/stylesheets/manager.scss | 106 +++++++++++----- .../stylesheets/personnes_impliquees.scss | 36 +----- app/assets/stylesheets/procedure_show.scss | 39 +----- 6 files changed, 87 insertions(+), 227 deletions(-) diff --git a/app/assets/stylesheets/carte.scss b/app/assets/stylesheets/carte.scss index 3de591ed2..a9def4820 100644 --- a/app/assets/stylesheets/carte.scss +++ b/app/assets/stylesheets/carte.scss @@ -10,10 +10,6 @@ } } -.form react-fragment[data-component-name='MapEditor'] [data-reach-combobox-input] { - margin-bottom: 0; -} - .map-style-control { position: absolute; bottom: 4px; diff --git a/app/assets/stylesheets/dsfr.scss b/app/assets/stylesheets/dsfr.scss index 4de7cf6e9..1423c8086 100644 --- a/app/assets/stylesheets/dsfr.scss +++ b/app/assets/stylesheets/dsfr.scss @@ -37,6 +37,15 @@ trix-editor.fr-input { } } +.fr-ds-combobox__multiple { + .fr-tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-bottom: 0.3rem; + } +} + .fr-ds-combobox__menu { &[data-placement=top] { --origin: translateY(8px); diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index c743405fd..fe0141c6c 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -356,41 +356,6 @@ margin-bottom: 0; } - [data-reach-combobox-input] { - &:not([class^='width-']) { - width: 100%; - min-width: 50%; - max-width: 100%; - } - - &:focus { - border-color: $blue-france-500; - } - } - - [data-reach-combobox-token-list] { - padding: $default-spacer; - display: flex; - flex-wrap: wrap; - align-items: center; - list-style: none; - } - - [data-reach-combobox-token] button { - border: solid 1px $border-grey; - border-radius: 4px; - padding: $default-spacer; - margin-right: $default-spacer; - cursor: pointer; - display: flex; - align-items: center; - } - - [data-reach-combobox-token] button:focus { - background-color: $black; - color: $white; - } - .editable-champ { &:not(.editable-champ-carte) .algolia-autocomplete { margin-bottom: 2 * $default-padding; @@ -524,91 +489,8 @@ } } -react-fragment[data-component-name^="ComboMultiple"] { +.fr-ds-combobox__multiple { margin-bottom: $default-fields-spacer; - - [data-reach-combobox-input] { - flex-grow: 1; - background-image: image-url("icons/chevron-down"); - background-size: 14px; - background-repeat: no-repeat; - background-position: right 10px center; - border-radius: 4px; - border: solid 1px $border-grey; - padding: $default-padding; - margin: $default-spacer; - margin-top: 0; - width: 100%; - } - - ul { - list-style: none; - - li { - margin-right: $default-spacer; - display: inline-block; - } - } -} - -[data-reach-combobox-token-label] { - border: 1px solid #CCCCCC; - border-radius: 4px; - display: flex; - flex-wrap: wrap; -} - -[data-reach-combobox-option] { - font-size: 16px; - list-style-type: none; -} - -[data-reach-combobox-option][aria-selected="true"] { - background: $light-blue !important; - color: $white; -} - -[data-reach-combobox-separator] { - font-size: 16px; - color: $dark-grey; - background: $light-grey; - padding: $default-spacer; -} - -[data-reach-combobox-no-results] { - font-size: 16px; - color: $dark-grey; - background: $light-grey; - padding: $default-spacer; -} - -[data-reach-combobox-token] button { - cursor: pointer; - background-color: transparent; - background-image: none; - border: none; - line-height: 1; - padding: 0; - margin-right: 4px; - display: flex; - align-items: center !important; -} - -[data-reach-combobox-input] button:focus { - outline-color: $light-blue; -} - -[data-fr-theme="dark"] [data-reach-combobox-popover] { - border: none; - background: var(--background-action-low-blue-france); -} - -[data-fr-theme="dark"] [data-reach-combobox-option]:hover { - background: var(--background-action-low-blue-france-hover); -} - -[data-reach-combobox-popover] { - z-index: 20; } .fconnect-form { diff --git a/app/assets/stylesheets/manager.scss b/app/assets/stylesheets/manager.scss index 7480fddd7..e6fe59c3b 100644 --- a/app/assets/stylesheets/manager.scss +++ b/app/assets/stylesheets/manager.scss @@ -1,36 +1,5 @@ @import "constants"; -[data-reach-combobox-token-label] { - border: 1px solid #CCCCCC; - border-radius: 4px; - display: flex; - flex-wrap: wrap; -} - -.form [data-reach-combobox-token-list] { - padding: 8px; - display: flex; - align-items: center; - list-style: none; -} - -.form [data-reach-combobox-input]:not([class^='width-']) { - width: 100%; - min-width: 50%; - max-width: 100%; -} - -.form [data-reach-combobox-token] button { - border: solid 1px #CCCCCC; - background-color: transparent; - border-radius: 4px; - padding: 8px; - margin-right: 8px; - cursor: pointer; - display: flex; - align-items: center; -} - .hidden { display: none; } @@ -70,4 +39,79 @@ margin-bottom: 4px; } } + + .fr-ds-combobox { + .fr-autocomplete { + background-repeat: no-repeat; + background-position: right; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z'%3E%3C/path%3E%3C/svg%3E"); + } + } + + .fr-ds-combobox__multiple { + .fr-tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + margin-bottom: 0.3rem; + } + + .fr-tag { + font-size: small; + padding: 0.5rem; + display: flex; + align-items: center; + border: solid 1px #dcdcdc; + + button { + margin-left: 0.3rem; + } + } + } + + .fr-ds-combobox__menu { + &[data-placement=top] { + --origin: translateY(8px); + } + + &[data-placement=bottom] { + --origin: translateY(-8px); + } + + &[data-placement=right] { + --origin: translateX(-8px); + } + + &[data-placement=left] { + --origin: translateX(8px); + } + + &[data-entering] { + animation: popover-slide 200ms; + } + + &.fr-menu { + width: var(--trigger-width); + top: unset; + background-color: white; + border: solid 1px #dcdcdc; + + .fr-menu__list { + display: block; + width: unset; + max-height: 300px; + overflow: auto; + } + + .fr-menu__item { + &[data-selected] { + font-weight: bold; + } + + &[data-focused] { + font-weight: bold; + } + } + } + } } diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss index 990876d80..42f3d07f8 100644 --- a/app/assets/stylesheets/personnes_impliquees.scss +++ b/app/assets/stylesheets/personnes_impliquees.scss @@ -9,41 +9,7 @@ margin-left: 16px; } - react-fragment[data-component-name^="ComboMultiple"] { + .fr-ds-combobox__multiple { margin-bottom: 0; - - [data-reach-combobox-token-list] { - padding: 0.5 * $default-padding; - display: flex; - } - - [data-reach-combobox-token] button { - border: solid 1px $border-grey; - margin-top: 0.5 * $default-padding; - margin-bottom: 0.5 * $default-padding; - margin-right: 0.5 * $default-padding; - border-radius: 4px; - padding: 0.5 * $default-padding; - cursor: pointer; - list-style: none; - } - - [data-reach-combobox-token] button:focus { - background-color: $black; - color: $white; - } - - - [data-reach-combobox-input] { - outline: none; - border: none; - flex-grow: 1; - margin: 0.25rem; - } - - [data-reach-combobox-input]:focus { - outline: solid; - outline-color: $light-blue; - } } } diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss index 8b27c3436..bc3ec8730 100644 --- a/app/assets/stylesheets/procedure_show.scss +++ b/app/assets/stylesheets/procedure_show.scss @@ -45,45 +45,8 @@ display: inline-block; } - react-fragment[data-component-name^="ComboMultiple"] { + .fr-ds-combobox__multiple { margin-bottom: $default-fields-spacer; - - [data-reach-combobox-token-list] { - padding: 0.25 * $default-padding; - display: inline-block; - width: 100%; - } - - [data-reach-combobox-token] button { - border: solid 1px $border-grey; - margin: 0.25 * $default-padding; - border-radius: 2px; - padding: 0.25 * $default-padding; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - } - - [data-reach-combobox-token] button:focus { - background-color: $black; - color: $white; - } - - - [data-reach-combobox-input] { - outline: none; - flex-grow: 1; - margin: $default-spacer; - padding: $default-spacer; - border-radius: 4px; - border: solid 1px $border-grey; - margin-top: 0; - } - - [data-reach-combobox-input]:focus { - border-color: $blue-france-500; - } } // fix/dsfr From ecc847ae3dd10258f560acc04fa27bc3e81a172b Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 30 May 2024 11:44:59 +0200 Subject: [PATCH 20/39] chore(vite): add vite-bundle-visualizer --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f773fc21..1029ea7db 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,8 @@ "postinstall": "patch-package", "test": "vitest", "coverage": "vitest run --coverage", - "up": "bunx npm-check-updates --root --format group -i" + "up": "bunx npm-check-updates --root --format group -i", + "vite-bundle-visualizer": "bunx vite-bundle-visualizer" }, "resolutions": { "string-width": "4.2.2", From 89fb0abe6ec9e71b3a4f4b8279a2bb45173c2f42 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 21 Jun 2024 17:03:15 +0200 Subject: [PATCH 21/39] fix(spec): fix system test --- .../controllers/turbo_controller.ts | 4 + .../cassettes/The_user/fill_a_dossier.yml | 639 +++++------------- spec/system/users/brouillon_spec.rb | 11 + 3 files changed, 189 insertions(+), 465 deletions(-) diff --git a/app/javascript/controllers/turbo_controller.ts b/app/javascript/controllers/turbo_controller.ts index d210b3933..58e298294 100644 --- a/app/javascript/controllers/turbo_controller.ts +++ b/app/javascript/controllers/turbo_controller.ts @@ -62,6 +62,10 @@ export class TurboController extends ApplicationController { // They allow us to preserve certain HTML changes across mutations. this.#actions.observe(); + this.#actions.ready().then(() => { + document.body.classList.add('dom-ready'); + }); + // setup spinner events this.onGlobal('turbo:submit-start', () => this.startSpinner()); this.onGlobal('turbo:submit-end', () => this.stopSpinner()); diff --git a/spec/fixtures/cassettes/The_user/fill_a_dossier.yml b/spec/fixtures/cassettes/The_user/fill_a_dossier.yml index 2bf10a941..ea0d2acf2 100644 --- a/spec/fixtures/cassettes/The_user/fill_a_dossier.yml +++ b/spec/fixtures/cassettes/The_user/fill_a_dossier.yml @@ -1,77 +1,5 @@ --- http_interactions: -- request: - method: get - uri: https://geo.api.gouv.fr/communes?boost=population&codePostal=60&limit=50&type=commune-actuelle,arrondissement-municipal - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.10.3 (Ubuntu) - Date: - - Mon, 04 Mar 2024 09:41:10 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '2' - X-Powered-By: - - Express - Vary: - - Origin - Etag: - - W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w" - Strict-Transport-Security: - - max-age=15552000 - body: - encoding: ASCII-8BIT - string: "[]" - recorded_at: Mon, 04 Mar 2024 09:41:10 GMT -- request: - method: get - uri: https://geo.api.gouv.fr/communes?boost=population&codePostal=6040&limit=50&type=commune-actuelle,arrondissement-municipal - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.10.3 (Ubuntu) - Date: - - Mon, 04 Mar 2024 09:41:10 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '2' - X-Powered-By: - - Express - Vary: - - Origin - Etag: - - W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w" - Strict-Transport-Security: - - max-age=15552000 - body: - encoding: ASCII-8BIT - string: "[]" - recorded_at: Mon, 04 Mar 2024 09:41:10 GMT - request: method: get uri: https://geo.api.gouv.fr/communes?boost=population&codePostal=60400&limit=50&type=commune-actuelle,arrondissement-municipal @@ -91,7 +19,7 @@ http_interactions: Server: - nginx/1.10.3 (Ubuntu) Date: - - Mon, 04 Mar 2024 09:41:11 GMT + - Tue, 02 Jul 2024 13:53:51 GMT Content-Type: - application/json; charset=utf-8 Content-Length: @@ -102,310 +30,14 @@ http_interactions: X-Powered-By: - Express Etag: - - W/"10fd-5D0Cm9Wh2PWHu/iLOAIRod2IvrQ" + - W/"10fd-b5NvAPTb7NhRASMMh9m0aHfdfMU" Strict-Transport-Security: - max-age=15552000 body: encoding: ASCII-8BIT string: !binary |- - W3sibm9tIjoiQXBwaWxseSIsImNvZGUiOiI2MDAyMSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDAyMTYiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1Mjl9LHsibm9tIjoiQmFixZN1ZiIsImNvZGUiOiI2MDAzNyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDAzNjQiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1MTB9LHsibm9tIjoiQmVhdXJhaW5zLWzDqHMtTm95b24iLCJjb2RlIjoiNjAwNTUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAwNTQ3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MzQwfSx7Im5vbSI6IkLDqWjDqXJpY291cnQiLCJjb2RlIjoiNjAwNTkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAwNTg4IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MjAyfSx7Im5vbSI6IkJyw6l0aWdueSIsImNvZGUiOiI2MDEwNSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDEwNTciLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo0Mjd9LHsibm9tIjoiQnVzc3kiLCJjb2RlIjoiNjAxMTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAxMTcyIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MzEwfSx7Im5vbSI6IkNhaXNuZXMiLCJjb2RlIjoiNjAxMTgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAxMTgwIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NTA3fSx7Im5vbSI6IkNyaXNvbGxlcyIsImNvZGUiOiI2MDE4MSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDE4MDAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo5MDh9LHsibm9tIjoiQ3V0cyIsImNvZGUiOiI2MDE4OSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDE4ODMiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo5Njd9LHsibm9tIjoiR2VudnJ5IiwiY29kZSI6IjYwMjcwIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwMjY3NSIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjMzNX0seyJub20iOiJHcmFuZHLDuyIsImNvZGUiOiI2MDI4NyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDI4NDAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozNjB9LHsibm9tIjoiTGFyYnJveWUiLCJjb2RlIjoiNjAzNDgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAzNDY3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NTE2fSx7Im5vbSI6Ik1vbmRlc2NvdXJ0IiwiY29kZSI6IjYwNDEwIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNDA2OSIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjI0OX0seyJub20iOiJNb3JsaW5jb3VydCIsImNvZGUiOiI2MDQzMSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDQyNjciLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1Mzh9LHsibm9tIjoiTmFtcGNlbCIsImNvZGUiOiI2MDQ0NSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDQ0MDgiLCJjb2RlRXBjaSI6IjI0NjAwMDc0OSIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozMTB9LHsibm9tIjoiTm95b24iLCJjb2RlIjoiNjA0NzEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA0NjU1IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MTMxOTd9LHsibm9tIjoiUGFzc2VsIiwiY29kZSI6IjYwNDg4IiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNDgyMCIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjI3Mn0seyJub20iOiJQb250LWwnw4l2w6pxdWUiLCJjb2RlIjoiNjA1MDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA1MDA5IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6Njc5fSx7Im5vbSI6IlBvbnRvaXNlLWzDqHMtTm95b24iLCJjb2RlIjoiNjA1MDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA1MDE3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NDUzfSx7Im5vbSI6IlBvcnF1w6lyaWNvdXJ0IiwiY29kZSI6IjYwNTExIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNTA1OCIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjQwMX0seyJub20iOiJTYWxlbmN5IiwiY29kZSI6IjYwNjAzIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNTk2NyIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjg5OH0seyJub20iOiJTZW1waWdueSIsImNvZGUiOiI2MDYxMCIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDYwMTUiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo3NjZ9LHsibm9tIjoiU2VybWFpemUiLCJjb2RlIjoiNjA2MTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA2MDgwIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MjczfSx7Im5vbSI6IlN1em95IiwiY29kZSI6IjYwNjI1IiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNjE2MyIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjU3NH0seyJub20iOiJWYXJlc25lcyIsImNvZGUiOiI2MDY1NSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDY0NjAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozNjR9LHsibm9tIjoiVmF1Y2hlbGxlcyIsImNvZGUiOiI2MDY1NyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDY0ODYiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjoyNDZ9LHsibm9tIjoiVmlsbGUiLCJjb2RlIjoiNjA2NzYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA2Njc2IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NzU1fV0= - recorded_at: Mon, 04 Mar 2024 09:41:10 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20d - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4449' - Vary: - - Origin - Etag: - - W/"1161-ye3cMV7bYtodrssf15NEDin0/8U" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzIuMjY5MjQyLDQ5LjkwNjQ3N119LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGQnQWJiZXZpbGxlIDgwMDAwIEFtaWVucyIsInNjb3JlIjowLjg5NTIyMDkwOTA5MDkwOSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiODAwMjFfMDA1MF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZCdBYmJldmlsbGUiLCJwb3N0Y29kZSI6IjgwMDAwIiwiY2l0eWNvZGUiOiI4MDAyMSIsIngiOjY0NzQ2My4yNiwieSI6Njk3ODg4Ni41NCwiY2l0eSI6IkFtaWVucyIsImNvbnRleHQiOiI4MCwgU29tbWUsIEhhdXRzLWRlLUZyYW5jZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODQ3NDMsInN0cmVldCI6IlJ1ZSBkJ0FiYmV2aWxsZSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlswLjY4NDM5Niw0Ny4zODczMjddfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBkJ0VudHJhaWd1ZXMgMzcwMDAgVG91cnMiLCJzY29yZSI6MC44OTM2NTcyNzI3MjcyNzI3LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzNzI2MV8xNjgwXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBkJ0VudHJhaWd1ZXMiLCJwb3N0Y29kZSI6IjM3MDAwIiwiY2l0eWNvZGUiOiIzNzI2MSIsIngiOjUyNTMzMi4wNywieSI6NjcwMTExNS4yOCwiY2l0eSI6IlRvdXJzIiwiY29udGV4dCI6IjM3LCBJbmRyZS1ldC1Mb2lyZSwgQ2VudHJlLVZhbCBkZSBMb2lyZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODMwMjMsInN0cmVldCI6IlJ1ZSBkJ0VudHJhaWd1ZXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMy4wNjExMzYsNTAuNjIzNTI0XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZCdBcnRvaXMgNTkwMDAgTGlsbGUiLCJzY29yZSI6MC44OTM1MDk5OTk5OTk5OTk5LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiI1OTM1MF8wMzkxXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBkJ0FydG9pcyIsInBvc3Rjb2RlIjoiNTkwMDAiLCJjaXR5Y29kZSI6IjU5MzUwIiwib2xkY2l0eWNvZGUiOiI1OTM1MCIsIngiOjcwNDMzMy41MSwieSI6NzA1ODUwNC4zMywiY2l0eSI6IkxpbGxlIiwib2xkY2l0eSI6IkxpbGxlIiwiY29udGV4dCI6IjU5LCBOb3JkLCBIYXV0cy1kZS1GcmFuY2UiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjgyODYxLCJzdHJlZXQiOiJSdWUgZCdBcnRvaXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNS4wNTEyOTksNDcuMzEyMzA5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZCdBdXhvbm5lIDIxMDAwIERpam9uIiwic2NvcmUiOjAuODkzMjEwOTA5MDkwOTA5LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIyMTIzMV8wNjEwXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBkJ0F1eG9ubmUiLCJwb3N0Y29kZSI6IjIxMDAwIiwiY2l0eWNvZGUiOiIyMTIzMSIsIngiOjg1NDk1Mi40MSwieSI6NjY5MjIzMy41MSwiY2l0eSI6IkRpam9uIiwiY29udGV4dCI6IjIxLCBDw7R0ZS1kJ09yLCBCb3VyZ29nbmUtRnJhbmNoZS1Db210w6kiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjgyNTMyLCJzdHJlZXQiOiJSdWUgZCdBdXhvbm5lIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuNDY2NSw0My42MDY1NjVdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBkJ0Fzc2FsaXQgMzE1MDAgVG91bG91c2UiLCJzY29yZSI6MC44OTI2MiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzE1NTVfMDU4MF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZCdBc3NhbGl0IiwicG9zdGNvZGUiOiIzMTUwMCIsImNpdHljb2RlIjoiMzE1NTUiLCJ4Ijo1NzYxNjIuNSwieSI6NjI3OTgxNC45OCwiY2l0eSI6IlRvdWxvdXNlIiwiY29udGV4dCI6IjMxLCBIYXV0ZS1HYXJvbm5lLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjgxODgyLCJzdHJlZXQiOiJSdWUgZCdBc3NhbGl0In19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6Wy0xLjUzODcxNiw0Ny4yMjAyNjldfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBkJ0FsbG9udmlsbGUgNDQwMDAgTmFudGVzIiwic2NvcmUiOjAuODkyNDYzNjM2MzYzNjM2MywiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNDQxMDlfMDE2OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZCdBbGxvbnZpbGxlIiwicG9zdGNvZGUiOiI0NDAwMCIsImNpdHljb2RlIjoiNDQxMDkiLCJ4IjozNTY3MTQuMjcsInkiOjY2ODk4NjUuNTUsImNpdHkiOiJOYW50ZXMiLCJjb250ZXh0IjoiNDQsIExvaXJlLUF0bGFudGlxdWUsIFBheXMgZGUgbGEgTG9pcmUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjgxNzEsInN0cmVldCI6IlJ1ZSBkJ0FsbG9udmlsbGUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi4zMjQ4NzQsNDguODI4NjEyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZCdBbMOpc2lhIDc1MDE0IFBhcmlzIiwic2NvcmUiOjAuODkyMTc3MjcyNzI3MjcyNywiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzUxMTRfMDE0M18wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZCdBbMOpc2lhIiwicG9zdGNvZGUiOiI3NTAxNCIsImNpdHljb2RlIjoiNzUxMTQiLCJ4Ijo2NTA0MzcuNDcsInkiOjY4NTg5NDAuMDgsImNpdHkiOiJQYXJpcyIsImRpc3RyaWN0IjoiUGFyaXMgMTRlIEFycm9uZGlzc2VtZW50IiwiY29udGV4dCI6Ijc1LCBQYXJpcywgw45sZS1kZS1GcmFuY2UiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjgxMzk1LCJzdHJlZXQiOiJSdWUgZCdBbMOpc2lhIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzMuMDQxODM1LDUwLjYyMzI3NV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGQnRXNxdWVybWVzIDU5MDAwIExpbGxlIiwic2NvcmUiOjAuODkyMTcyNzI3MjcyNzI3MiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNTkzNTBfMzE5M18wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZCdFc3F1ZXJtZXMiLCJwb3N0Y29kZSI6IjU5MDAwIiwiY2l0eWNvZGUiOiI1OTM1MCIsIm9sZGNpdHljb2RlIjoiNTkzNTAiLCJ4Ijo3MDI5NjUuNDEsInkiOjcwNTg0NzUuNjksImNpdHkiOiJMaWxsZSIsIm9sZGNpdHkiOiJMaWxsZSIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44MTM5LCJzdHJlZXQiOiJSdWUgZCdFc3F1ZXJtZXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMy40MDM1OTUsNTAuMzQ5NzcyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgRCA1OTI1NSBIYXZlbHV5Iiwic2NvcmUiOjAuNTEzMzg2MTAzODk2MTAzOCwiaWQiOiI1OTI5Ml9naWk0eTQiLCJuYW1lIjoiUnVlIEQiLCJwb3N0Y29kZSI6IjU5MjU1IiwiY2l0eWNvZGUiOiI1OTI5MiIsIngiOjcyODc2My44NCwieSI6NzAyODA3OC41MywiY2l0eSI6IkhhdmVsdXkiLCJjb250ZXh0IjoiNTksIE5vcmQsIEhhdXRzLWRlLUZyYW5jZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjUwNDM5LCJzdHJlZXQiOiJSdWUgRCJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlszLjA2NjAzMiw1MC42MjExMzhdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkJ0FycmFzIDU5MDAwIExpbGxlIiwic2NvcmUiOjAuMzI2MjMwMDY5OTMwMDY5OSwiaWQiOiI1OTM1MF8wMzc2IiwibmFtZSI6IlJ1ZSBkJ0FycmFzIiwicG9zdGNvZGUiOiI1OTAwMCIsImNpdHljb2RlIjoiNTkzNTAiLCJvbGRjaXR5Y29kZSI6IjU5MzUwIiwieCI6NzA0NjgwLjc4LCJ5Ijo3MDU4MjM4Ljc0LCJjaXR5IjoiTGlsbGUiLCJvbGRjaXR5IjoiTGlsbGUiLCJjb250ZXh0IjoiNTksIE5vcmQsIEhhdXRzLWRlLUZyYW5jZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjgxOTMsInN0cmVldCI6IlJ1ZSBkJ0FycmFzIn19XSwiYXR0cmlidXRpb24iOiJCQU4iLCJsaWNlbmNlIjoiRVRBTEFCLTIuMCIsInF1ZXJ5IjoiNzggUnVlIGQiLCJsaW1pdCI6MTB9 - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20R - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '129' - Vary: - - Origin - Etag: - - W/"81-+5qJ3zMojnCP18TiVLMlqIkD8QM" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: '{"type":"FeatureCollection","version":"draft","features":[],"attribution":"BAN","licence":"ETALAB-2.0","query":"78 - R","limit":10}' - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4440' - Vary: - - Origin - Etag: - - W/"1158-z84zpIdEpc3bQAqfMwajMvgGvGQ" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzIuMzA1MzkxLDQ4Ljg0MzU3Nl19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIExlY291cmJlIDc1MDE1IFBhcmlzIiwic2NvcmUiOjAuODk1NzQ5OTk5OTk5OTk5OSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzUxMTVfNTQ1Nl8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgTGVjb3VyYmUiLCJwb3N0Y29kZSI6Ijc1MDE1IiwiY2l0eWNvZGUiOiI3NTExNSIsIngiOjY0OTAyMS44NiwieSI6Njg2MDYxNi4zMSwiY2l0eSI6IlBhcmlzIiwiZGlzdHJpY3QiOiJQYXJpcyAxNWUgQXJyb25kaXNzZW1lbnQiLCJjb250ZXh0IjoiNzUsIFBhcmlzLCDDjmxlLWRlLUZyYW5jZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODUzMjUsInN0cmVldCI6IlJ1ZSBMZWNvdXJiZSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstMC41NjEzMjUsNDQuODIyNjkzXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgUGVsbGVwb3J0IDMzODAwIEJvcmRlYXV4Iiwic2NvcmUiOjAuODk1NDcxODE4MTgxODE4MiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzMwNjNfNzEwNV8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgUGVsbGVwb3J0IiwicG9zdGNvZGUiOiIzMzgwMCIsImNpdHljb2RlIjoiMzMwNjMiLCJ4Ijo0MTg1NzcuMDYsInkiOjY0MjAwNzMuMjYsImNpdHkiOiJCb3JkZWF1eCIsImNvbnRleHQiOiIzMywgR2lyb25kZSwgTm91dmVsbGUtQXF1aXRhaW5lIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44NTAxOSwic3RyZWV0IjoiUnVlIFBlbGxlcG9ydCJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstMC41ODU1MzIsNDQuODQxMjQ5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgSnVkYcOvcXVlIDMzMDAwIEJvcmRlYXV4Iiwic2NvcmUiOjAuODk1MjIxODE4MTgxODE4MSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzMwNjNfNDgxMF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgSnVkYcOvcXVlIiwicG9zdGNvZGUiOiIzMzAwMCIsImNpdHljb2RlIjoiMzMwNjMiLCJ4Ijo0MTY3NTkuMDQsInkiOjY0MjIyMTguNzMsImNpdHkiOiJCb3JkZWF1eCIsImNvbnRleHQiOiIzMywgR2lyb25kZSwgTm91dmVsbGUtQXF1aXRhaW5lIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44NDc0NCwic3RyZWV0IjoiUnVlIEp1ZGHDr3F1ZSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlszLjA1OTc4OSw1MC42MzU5NzhdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBOYXRpb25hbGUgNTkwMDAgTGlsbGUiLCJzY29yZSI6MC44OTUxMiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNTkzNTBfZmtpMWY2XzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBOYXRpb25hbGUiLCJwb3N0Y29kZSI6IjU5MDAwIiwiY2l0eWNvZGUiOiI1OTM1MCIsIm9sZGNpdHljb2RlIjoiNTkzNTAiLCJ4Ijo3MDQyMzYuOTgsInkiOjcwNTk4OTEuOTksImNpdHkiOiJMaWxsZSIsIm9sZGNpdHkiOiJMaWxsZSIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44NDYzMiwic3RyZWV0IjoiUnVlIE5hdGlvbmFsZSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstMC42MDQ3MTUsNDQuODQ0MzQxXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgUGFzdGV1ciAzMzIwMCBCb3JkZWF1eCIsInNjb3JlIjowLjg5NTA5OTk5OTk5OTk5OTksImhvdXNlbnVtYmVyIjoiNzgiLCJpZCI6IjMzMDYzXzY5NzVfMDAwNzgiLCJuYW1lIjoiNzggUnVlIFBhc3RldXIiLCJwb3N0Y29kZSI6IjMzMjAwIiwiY2l0eWNvZGUiOiIzMzA2MyIsIngiOjQxNTI2MC4zOSwieSI6NjQyMjYzMC43OSwiY2l0eSI6IkJvcmRlYXV4IiwiY29udGV4dCI6IjMzLCBHaXJvbmRlLCBOb3V2ZWxsZS1BcXVpdGFpbmUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjg0NjEsInN0cmVldCI6IlJ1ZSBQYXN0ZXVyIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuNDE3MTE5LDQzLjU4NzI2MV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIFZlc3RyZXBhaW4gMzExMDAgVG91bG91c2UiLCJzY29yZSI6MC44OTQ5NzcyNzI3MjcyNzI3LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzMTU1NV84ODA0XzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBWZXN0cmVwYWluIiwicG9zdGNvZGUiOiIzMTEwMCIsImNpdHljb2RlIjoiMzE1NTUiLCJ4Ijo1NzIxMzIuMjcsInkiOjYyNzc3NDguNjUsImNpdHkiOiJUb3Vsb3VzZSIsImNvbnRleHQiOiIzMSwgSGF1dGUtR2Fyb25uZSwgT2NjaXRhbmllIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44NDQ3NSwic3RyZWV0IjoiUnVlIFZlc3RyZXBhaW4ifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS40NjA5MDYsNDMuNTc5MDg5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgQm9ubmF0IDMxNDAwIFRvdWxvdXNlIiwic2NvcmUiOjAuODk0OTQxODE4MTgxODE4MiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzE1NTVfMTI0NF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgQm9ubmF0IiwicG9zdGNvZGUiOiIzMTQwMCIsImNpdHljb2RlIjoiMzE1NTUiLCJ4Ijo1NzU2NTEuMywieSI6NjI3Njc3MC42NCwiY2l0eSI6IlRvdWxvdXNlIiwiY29udGV4dCI6IjMxLCBIYXV0ZS1HYXJvbm5lLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjg0NDM2LCJzdHJlZXQiOiJSdWUgQm9ubmF0In19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6Wy0wLjU2Mjk4OSw0NC44MjUxODddfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBNYWxiZWMgMzM4MDAgQm9yZGVhdXgiLCJzY29yZSI6MC44OTQ5MDE4MTgxODE4MTgxLCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzMzA2M181ODYwXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBNYWxiZWMiLCJwb3N0Y29kZSI6IjMzODAwIiwiY2l0eWNvZGUiOiIzMzA2MyIsIngiOjQxODQ1OC4xNSwieSI6NjQyMDM1NS45MiwiY2l0eSI6IkJvcmRlYXV4IiwiY29udGV4dCI6IjMzLCBHaXJvbmRlLCBOb3V2ZWxsZS1BcXVpdGFpbmUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjg0MzkyLCJzdHJlZXQiOiJSdWUgTWFsYmVjIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6Wy0wLjU4Njk5OSw0NC44MzM5MDddfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBMZWNvY3EgMzMwMDAgQm9yZGVhdXgiLCJzY29yZSI6MC44OTQ1NzgxODE4MTgxODE3LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzMzA2M181MzIwXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBMZWNvY3EiLCJwb3N0Y29kZSI6IjMzMDAwIiwiY2l0eWNvZGUiOiIzMzA2MyIsIngiOjQxNjYwNi4yLCJ5Ijo2NDIxNDA5LjM1LCJjaXR5IjoiQm9yZGVhdXgiLCJjb250ZXh0IjoiMzMsIEdpcm9uZGUsIE5vdXZlbGxlLUFxdWl0YWluZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODQwMzYsInN0cmVldCI6IlJ1ZSBMZWNvY3EifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbLTAuNTg3NTUzLDQ0Ljg0ODM4NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIE5hdWphYyAzMzAwMCBCb3JkZWF1eCIsInNjb3JlIjowLjg5NDU3ODE4MTgxODE4MTcsImhvdXNlbnVtYmVyIjoiNzgiLCJpZCI6IjMzMDYzXzY2NjBfMDAwNzgiLCJuYW1lIjoiNzggUnVlIE5hdWphYyIsInBvc3Rjb2RlIjoiMzMwMDAiLCJjaXR5Y29kZSI6IjMzMDYzIiwieCI6NDE2NjM1LjQ5LCJ5Ijo2NDIzMDE3LjY1LCJjaXR5IjoiQm9yZGVhdXgiLCJjb250ZXh0IjoiMzMsIEdpcm9uZGUsIE5vdXZlbGxlLUFxdWl0YWluZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODQwMzYsInN0cmVldCI6IlJ1ZSBOYXVqYWMifX1dLCJhdHRyaWJ1dGlvbiI6IkJBTiIsImxpY2VuY2UiOiJFVEFMQUItMi4wIiwicXVlcnkiOiI3OCBSdWUiLCJsaW1pdCI6MTB9 - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4186' - Vary: - - Origin - Etag: - - W/"105a-ebhd1czeXybk7JwL9w0CNhYfX38" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuOTU5NTA3MjcyNzI3MjcyNywiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS4wMDQzNTgsNDkuNDU5NDIzXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgR3LDqSA3NjM4MCBNb250aWdueSIsInNjb3JlIjowLjc5NzQ0MjAyNzk3MjAyOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzY0NDZfMDIwMF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqSIsInBvc3Rjb2RlIjoiNzYzODAiLCJjaXR5Y29kZSI6Ijc2NDQ2IiwieCI6NTU1MjgyLjI3LCJ5Ijo2OTMwNzE5LjU0LCJjaXR5IjoiTW9udGlnbnkiLCJjb250ZXh0IjoiNzYsIFNlaW5lLU1hcml0aW1lLCBOb3JtYW5kaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjQ2NDE3LCJzdHJlZXQiOiJSdWUgZHUgR3LDqSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstMC42NzQ3MDYsNDcuODE1MTAyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3LDqHMgNTMyMDAgQ2jDonRlYXUtR29udGllci1zdXItTWF5ZW5uZSIsInNjb3JlIjowLjY4MDkyMjQ0NzU1MjQ0NzYsImlkIjoiNTMwNjJfMTY5OCIsIm5hbWUiOiJSdWUgZHUgR3LDqHMiLCJwb3N0Y29kZSI6IjUzMjAwIiwiY2l0eWNvZGUiOiI1MzA2MiIsIm9sZGNpdHljb2RlIjoiNTMwMTQiLCJ4Ijo0MjUwODUuMDgsInkiOjY3NTI0NzYuNjYsImNpdHkiOiJDaMOidGVhdS1Hb250aWVyLXN1ci1NYXllbm5lIiwib2xkY2l0eSI6IkF6w6kiLCJjb250ZXh0IjoiNTMsIE1heWVubmUsIFBheXMgZGUgbGEgTG9pcmUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC41NjcwNywic3RyZWV0IjoiUnVlIGR1IEdyw6hzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuNzcwNjE5LDQzLjk1ODE5NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyw6hzIDMwMTMzIExlcyBBbmdsZXMiLCJzY29yZSI6MC42NzkzNDc5MDIwOTc5MDIxLCJpZCI6IjMwMDExX2tldHl2YSIsIm5hbWUiOiJSdWUgZHUgR3LDqHMiLCJwb3N0Y29kZSI6IjMwMTMzIiwiY2l0eWNvZGUiOiIzMDAxMSIsIngiOjg0MjEwNi44NiwieSI6NjMxOTI4MS4yNiwiY2l0eSI6IkxlcyBBbmdsZXMiLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjU0OTc1LCJzdHJlZXQiOiJSdWUgZHUgR3LDqHMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS44MTc5NzUsNDMuNjk5MzE3XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3LDqHMgODE1MDAgTGF2YXVyIiwic2NvcmUiOjAuNjc2NTg4ODExMTg4ODExMiwiaWQiOiI4MTE0MF8wMzgwIiwibmFtZSI6IlJ1ZSBkdSBHcsOocyIsInBvc3Rjb2RlIjoiODE1MDAiLCJjaXR5Y29kZSI6IjgxMTQwIiwieCI6NjA0Njk3LjY3LCJ5Ijo2Mjg5NjMzLjg4LCJjaXR5IjoiTGF2YXVyIiwiY29udGV4dCI6IjgxLCBUYXJuLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC41MTk0LCJzdHJlZXQiOiJSdWUgZHUgR3LDqHMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbLTEuOTU3OTEzLDQ2LjcwNzI0Nl19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyw6hzIDg1MjcwIFNhaW50LUhpbGFpcmUtZGUtUmlleiIsInNjb3JlIjowLjY3NDQzODgxMTE4ODgxMTIsImlkIjoiODUyMjZfMTI1NSIsIm5hbWUiOiJSdWUgZHUgR3LDqHMiLCJwb3N0Y29kZSI6Ijg1MjcwIiwiY2l0eWNvZGUiOiI4NTIyNiIsIngiOjMyMTQ3Mi44NiwieSI6NjYzNDkwMy43MSwiY2l0eSI6IlNhaW50LUhpbGFpcmUtZGUtUmlleiIsImNvbnRleHQiOiI4NSwgVmVuZMOpZSwgUGF5cyBkZSBsYSBMb2lyZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQ5NTc1LCJzdHJlZXQiOiJSdWUgZHUgR3LDqHMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbLTEuMDMzMjQ5LDQ4LjExODQxMV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyZXMgNTM0MTAgU2FpbnQtUGllcnJlLWxhLUNvdXIiLCJzY29yZSI6MC42NzQzNzMzNTY2NDMzNTY2LCJpZCI6IjUzMjQ3XzAwMTgiLCJuYW1lIjoiUnVlIGR1IEdyZXMiLCJwb3N0Y29kZSI6IjUzNDEwIiwiY2l0eWNvZGUiOiI1MzI0NyIsIngiOjQwMDAwNC41NSwieSI6Njc4NzQ0NS41MywiY2l0eSI6IlNhaW50LVBpZXJyZS1sYS1Db3VyIiwiY29udGV4dCI6IjUzLCBNYXllbm5lLCBQYXlzIGRlIGxhIExvaXJlIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNDk1MDMsInN0cmVldCI6IlJ1ZSBkdSBHcmVzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMDMxNTIyLDQzLjY2NDY2M119LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyw6lzIDM0NjcwIFNhaW50LUJyw6hzIiwic2NvcmUiOjAuNjcyNDkyNDQ3NTUyNDQ3NSwiaWQiOiIzNDI0NF8wMDI3IiwibmFtZSI6IlJ1ZSBkdSBHcsOpcyIsInBvc3Rjb2RlIjoiMzQ2NzAiLCJjaXR5Y29kZSI6IjM0MjQ0IiwieCI6NzgzMjE4Ljg4LCJ5Ijo2Mjg1NjEyLjg4LCJjaXR5IjoiU2FpbnQtQnLDqHMiLCJjb250ZXh0IjoiMzQsIEjDqXJhdWx0LCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC40NzQzNCwic3RyZWV0IjoiUnVlIGR1IEdyw6lzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzMuODk5NDI1LDQzLjg2MDE4NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyw6hzIDM0MjcwIENsYXJldCIsInNjb3JlIjowLjY3MTA1MjQ0NzU1MjQ0NzYsImlkIjoiMzQwNzhfMDE1OSIsIm5hbWUiOiJSdWUgZHUgR3LDqHMiLCJwb3N0Y29kZSI6IjM0MjcwIiwiY2l0eWNvZGUiOiIzNDA3OCIsIngiOjc3MjMxNC44NSwieSI6NjMwNzIwOS4wOSwiY2l0eSI6IkNsYXJldCIsImNvbnRleHQiOiIzNCwgSMOpcmF1bHQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQ1ODUsInN0cmVldCI6IlJ1ZSBkdSBHcsOocyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0LjAxNjY4Miw0OS4wMjU0NzFdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkdSBHcmVzIDUxNTMwIENob3VpbGx5Iiwic2NvcmUiOjAuNjY5MDgzMzU2NjQzMzU2NiwiaWQiOiI1MTE1M19ieXNiZ2IiLCJuYW1lIjoiUnVlIGR1IEdyZXMiLCJwb3N0Y29kZSI6IjUxNTMwIiwiY2l0eWNvZGUiOiI1MTE1MyIsIngiOjc3NDM1My45MiwieSI6Njg4MTA5OC4wNCwiY2l0eSI6IkNob3VpbGx5IiwiY29udGV4dCI6IjUxLCBNYXJuZSwgR3JhbmQgRXN0IiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNDM2ODQsInN0cmVldCI6IlJ1ZSBkdSBHcmVzIn19XSwiYXR0cmlidXRpb24iOiJCQU4iLCJsaWNlbmNlIjoiRVRBTEFCLTIuMCIsInF1ZXJ5IjoiNzggUnVlIGR1IEdyw6lzIiwibGltaXQiOjEwfQ== - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%203 - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4362' - Vary: - - Origin - Etag: - - W/"110a-cDKu3ljEy++lztESVOHNYLZfaLE" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzIuNzA3MjMxLDQ4LjQ3NjIxNV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGRlcyBHcmVzIDc3NTkwIEJvaXMtbGUtUm9pIiwic2NvcmUiOjAuNDg1NjE1OTg5MzA0ODEyOCwiaWQiOiI3NzAzN18wNDIwIiwibmFtZSI6IlJ1ZSBkZXMgR3JlcyIsInBvc3Rjb2RlIjoiNzc1OTAiLCJjaXR5Y29kZSI6Ijc3MDM3IiwieCI6Njc4MzYxLjY5LCJ5Ijo2ODE5NTkwLjQxLCJjaXR5IjoiQm9pcy1sZS1Sb2kiLCJjb250ZXh0IjoiNzcsIFNlaW5lLWV0LU1hcm5lLCDDjmxlLWRlLUZyYW5jZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjU3NzA3LCJzdHJlZXQiOiJSdWUgZGVzIEdyZXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi41NzA1NSw0OC41MjYyMDZdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkZXMgR3LDqHMgNzczMTAgQm9pc3Npc2UtbGUtUm9pIiwic2NvcmUiOjAuNDc0OTc3ODA3NDg2NjMxMDQsImlkIjoiNzcwNDBfMDIyOCIsIm5hbWUiOiJSdWUgZGVzIEdyw6hzIiwicG9zdGNvZGUiOiI3NzMxMCIsImNpdHljb2RlIjoiNzcwNDAiLCJ4Ijo2NjgyOTAuMDIsInkiOjY4MjUxOTMuNjEsImNpdHkiOiJCb2lzc2lzZS1sZS1Sb2kiLCJjb250ZXh0IjoiNzcsIFNlaW5lLWV0LU1hcm5lLCDDjmxlLWRlLUZyYW5jZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQ2MDA1LCJzdHJlZXQiOiJSdWUgZGVzIEdyw6hzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMDc1MzI4LDQ4LjI5Mzg4NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyb3MgUmFpc2luIDEwMDAwIFRyb3llcyIsInNjb3JlIjowLjQyNzA5NzE5MDA4MjY0NDYsImlkIjoiMTAzODdfMjM1MCIsIm5hbWUiOiJSdWUgZHUgR3JvcyBSYWlzaW4iLCJwb3N0Y29kZSI6IjEwMDAwIiwiY2l0eWNvZGUiOiIxMDM4NyIsIngiOjc3OTc1MC4yOCwieSI6Njc5OTgyNi45MiwiY2l0eSI6IlRyb3llcyIsImNvbnRleHQiOiIxMCwgQXViZSwgR3JhbmQgRXN0IiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNjA3MTYsInN0cmVldCI6IlJ1ZSBkdSBHcm9zIFJhaXNpbiJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlswLjA2NDEwOCw0NS42NDM3NV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyb3MgQ2jDqm5lIDE2NzMwIFRyb2lzLVBhbGlzIiwic2NvcmUiOjAuMzcxMjgxNjUyODkyNTYyLCJpZCI6IjE2Mzg4XzAxNjAiLCJuYW1lIjoiUnVlIGR1IEdyb3MgQ2jDqm5lIiwicG9zdGNvZGUiOiIxNjczMCIsImNpdHljb2RlIjoiMTYzODgiLCJ4Ijo0NzEzNjQuNDEsInkiOjY1MDkxNjIuOTYsImNpdHkiOiJUcm9pcy1QYWxpcyIsImNvbnRleHQiOiIxNiwgQ2hhcmVudGUsIE5vdXZlbGxlLUFxdWl0YWluZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQwMjI4LCJzdHJlZXQiOiJSdWUgZHUgR3JvcyBDaMOqbmUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS42OTIxODEsNDguNDk0NjgzXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZGVzIDMgTWFyZXMgMjg3MDAgTGUgR3XDqS1kZS1Mb25ncm9pIiwic2NvcmUiOjAuMzQ2NzEwMTI5ODcwMTI5ODQsImlkIjoiMjgxODhfMDA3MiIsIm5hbWUiOiJSdWUgZGVzIDMgTWFyZXMiLCJwb3N0Y29kZSI6IjI4NzAwIiwiY2l0eWNvZGUiOiIyODE4OCIsIngiOjYwMzM3OC40MSwieSI6NjgyMjQwMy4zNywiY2l0eSI6IkxlIEd1w6ktZGUtTG9uZ3JvaSIsImNvbnRleHQiOiIyOCwgRXVyZS1ldC1Mb2lyLCBDZW50cmUtVmFsIGRlIExvaXJlIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMzg1MjQsInN0cmVldCI6IlJ1ZSBkZXMgMyBNYXJlcyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls1LjA1MjUyNCw0My40MTA2NzddfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IkhsbSBsZSBHcsOocyAzIDEzNTAwIE1hcnRpZ3VlcyIsInNjb3JlIjowLjMzMzU2MzYzNjM2MzYzNjM0LCJ0eXBlIjoibG9jYWxpdHkiLCJpbXBvcnRhbmNlIjowLjUxOTIsImlkIjoiMTMwNTZfQTEyMCIsIm5hbWUiOiJIbG0gbGUgR3LDqHMgMyIsInBvc3Rjb2RlIjoiMTM1MDAiLCJjaXR5Y29kZSI6IjEzMDU2IiwieCI6ODY2MzA4LjgxLCJ5Ijo2MjU5MDAxLjYsImNpdHkiOiJNYXJ0aWd1ZXMiLCJjb250ZXh0IjoiMTMsIEJvdWNoZXMtZHUtUmjDtG5lLCBQcm92ZW5jZS1BbHBlcy1Dw7R0ZSBkJ0F6dXIiLCJsb2NhbGl0eSI6IkhsbSBsZSBHcsOocyAzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzAuODA1MjAzLDQ5Ljg3MzMzMl19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IDMgUmd0IGRlcyBEcmFnb25zIDc2OTgwIFZldWxlcy1sZXMtUm9zZXMiLCJzY29yZSI6MC4zMTg4NDMyNDY3NTMyNDY3NywiaWQiOiI3NjczNV8wMjY1IiwibmFtZSI6IlJ1ZSBkdSAzIFJndCBkZXMgRHJhZ29ucyIsInBvc3Rjb2RlIjoiNzY5ODAiLCJjaXR5Y29kZSI6Ijc2NzM1IiwieCI6NTQyMTIzLjksInkiOjY5NzcxNDguMDgsImNpdHkiOiJWZXVsZXMtbGVzLVJvc2VzIiwiY29udGV4dCI6Ijc2LCBTZWluZS1NYXJpdGltZSwgTm9ybWFuZGllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMjkyOTksInN0cmVldCI6IlJ1ZSBkdSAzIFJndCBkZXMgRHJhZ29ucyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0Ljk1MDQ1Myw0NC4yODY3NDNdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlpBIGR1IEdyZCBEZXbDqXMgQWxsZWUgMyAyNjc5MCBUdWxldHRlIiwic2NvcmUiOjAuMjk1NzA5NzQwMjU5NzQwMywiaWQiOiIyNjM1N19CMDU1IiwibmFtZSI6IlpBIGR1IEdyZCBEZXbDqXMgQWxsZWUgMyIsInBvc3Rjb2RlIjoiMjY3OTAiLCJjaXR5Y29kZSI6IjI2MzU3IiwieCI6ODU1NjM1LjY1LCJ5Ijo2MzU2MTEzLjU5LCJjaXR5IjoiVHVsZXR0ZSIsImNvbnRleHQiOiIyNiwgRHLDtG1lLCBBdXZlcmduZS1SaMO0bmUtQWxwZXMiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC4zNTk5NSwic3RyZWV0IjoiWkEgZHUgR3JkIERldsOpcyBBbGxlZSAzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuMjMwNjk4LDQ1LjI4ODkxM119LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUm91dGUgZGVzIEdyw6hzIFJvdWdlcyAyNDE2MCBTYWludGUtVHJpZSIsInNjb3JlIjowLjI2NzA2MzMzMzMzMzMzMzMsImlkIjoiMjQ1MDdfeTVpNjYxIiwibmFtZSI6IlJvdXRlIGRlcyBHcsOocyBSb3VnZXMiLCJwb3N0Y29kZSI6IjI0MTYwIiwiY2l0eWNvZGUiOiIyNDUwNyIsIngiOjU2MTMxMC42OCwieSI6NjQ2NzA1OS43NywiY2l0eSI6IlNhaW50ZS1UcmllIiwiY29udGV4dCI6IjI0LCBEb3Jkb2duZSwgTm91dmVsbGUtQXF1aXRhaW5lIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMjcxMDMsInN0cmVldCI6IlJvdXRlIGRlcyBHcsOocyBSb3VnZXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMC4wMjQ5NDcsNDcuMDYwODE4XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3XDqSBTYWludGUtTWFyaWUgODYxMjAgTGVzIFRyb2lzLU1vdXRpZXJzIiwic2NvcmUiOjAuMjI0MDIzNjM2MzYzNjM2MzcsImlkIjoiODYyNzRfMDcyMCIsIm5hbWUiOiJSdWUgZHUgR3XDqSBTYWludGUtTWFyaWUiLCJwb3N0Y29kZSI6Ijg2MTIwIiwiY2l0eWNvZGUiOiI4NjI3NCIsIngiOjQ3NDI0NC4wOCwieSI6NjY2NjUzOS4zMiwiY2l0eSI6IkxlcyBUcm9pcy1Nb3V0aWVycyIsImNvbnRleHQiOiI4NiwgVmllbm5lLCBOb3V2ZWxsZS1BcXVpdGFpbmUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC4zNjQyNiwic3RyZWV0IjoiUnVlIGR1IEd1w6kgU2FpbnRlLU1hcmllIn19XSwiYXR0cmlidXRpb24iOiJCQU4iLCJsaWNlbmNlIjoiRVRBTEFCLTIuMCIsInF1ZXJ5IjoiNzggUnVlIGR1IEdyw6lzIDMiLCJsaW1pdCI6MTB9 - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%20303 - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4247' - Vary: - - Origin - Etag: - - W/"1097-sr8xmITTS06AybsOH3rSsirtCrw" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuODY4NTk4MTgxODE4MTgxOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNC40NzQ5NDYsNDQuMDk1MjA5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3LDqHMgMzAzMzAgTGEgQmFzdGlkZS1kJ0VuZ3JhcyIsInNjb3JlIjowLjYyOTM4ODA4NjEyNDQwMTgsImlkIjoiMzAwMzFfMDA0OCIsIm5hbWUiOiJSdWUgZHUgR3LDqHMiLCJwb3N0Y29kZSI6IjMwMzMwIiwiY2l0eWNvZGUiOiIzMDAzMSIsIngiOjgxODA5NS4zNCwieSI6NjMzNDAxNC43MywiY2l0eSI6IkxhIEJhc3RpZGUtZCdFbmdyYXMiLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjI5MTY5LCJzdHJlZXQiOiJSdWUgZHUgR3LDqHMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNC4wOTgwMTYsNDQuMTcxNzI5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJDaGVtaW4gZHUgR3LDqHMgMzAzNDAgU2FpbnQtSnVsaWVuLWxlcy1Sb3NpZXJzIiwic2NvcmUiOjAuNDExNzk3MjcyNzI3MjcyNzMsImlkIjoiMzAyNzRfMDIzMCIsIm5hbWUiOiJDaGVtaW4gZHUgR3LDqHMiLCJwb3N0Y29kZSI6IjMwMzQwIiwiY2l0eWNvZGUiOiIzMDI3NCIsIngiOjc4Nzc5OS41MywieSI6NjM0MjAyMy44OCwiY2l0eSI6IlNhaW50LUp1bGllbi1sZXMtUm9zaWVycyIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNTY5NzcsInN0cmVldCI6IkNoZW1pbiBkdSBHcsOocyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0LjU1ODU4Miw0My44MzI2MzRdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkdSBHcsOocyBkZXMgT2xpdmllcnMgMzAzMDAgSm9ucXVpw6hyZXMtU2FpbnQtVmluY2VudCIsInNjb3JlIjowLjQwMzgyODE4MTgxODE4MTgsImlkIjoiMzAxMzVfMDEyMiIsIm5hbWUiOiJSdWUgZHUgR3LDqHMgZGVzIE9saXZpZXJzIiwicG9zdGNvZGUiOiIzMDMwMCIsImNpdHljb2RlIjoiMzAxMzUiLCJ4Ijo4MjUzNjYuODcsInkiOjYzMDQ5NzMuMTEsImNpdHkiOiJKb25xdWnDqHJlcy1TYWludC1WaW5jZW50IiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC41MDQ2MSwic3RyZWV0IjoiUnVlIGR1IEdyw6hzIGRlcyBPbGl2aWVycyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0LjE0MTI1OCw0NC4xMDc4ODVdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IkNoZW1pbiBkdSBHcsOocyAzMDM0MCBNw6lqYW5uZXMtbMOocy1BbMOocyIsInNjb3JlIjowLjQwMzI1NTQ1NDU0NTQ1NDUzLCJpZCI6IjMwMTY1XzAwNjciLCJuYW1lIjoiQ2hlbWluIGR1IEdyw6hzIiwicG9zdGNvZGUiOiIzMDM0MCIsImNpdHljb2RlIjoiMzAxNjUiLCJ4Ijo3OTEzNTkuNTMsInkiOjYzMzQ5ODAuMzUsImNpdHkiOiJNw6lqYW5uZXMtbMOocy1BbMOocyIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNDc1ODEsInN0cmVldCI6IkNoZW1pbiBkdSBHcsOocyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0Ljc5MjcyNSw0My45NzY1NzVdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IkltcGFzc2UgZHUgR3LDqXMgMzA0MDAgVmlsbGVuZXV2ZS1sw6hzLUF2aWdub24iLCJzY29yZSI6MC40MDE4NTU2NjQzMzU2NjQzLCJpZCI6IjMwMzUxXzA1OTAiLCJuYW1lIjoiSW1wYXNzZSBkdSBHcsOpcyIsInBvc3Rjb2RlIjoiMzA0MDAiLCJjaXR5Y29kZSI6IjMwMzUxIiwieCI6ODQzODM0LjM4LCJ5Ijo2MzIxMzYzLjE2LCJjaXR5IjoiVmlsbGVuZXV2ZS1sw6hzLUF2aWdub24iLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjYxMjcyLCJzdHJlZXQiOiJJbXBhc3NlIGR1IEdyw6lzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMTI1MDMsNDMuOTY2OTg3XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJDaGVtaW4gZHUgR3LDqXMgMzAzNTAgQWlncmVtb250Iiwic2NvcmUiOjAuMzk4MTEyNzI3MjcyNzI3MywiaWQiOiIzMDAwMl8wMDUzIiwibmFtZSI6IkNoZW1pbiBkdSBHcsOpcyIsInBvc3Rjb2RlIjoiMzAzNTAiLCJjaXR5Y29kZSI6IjMwMDAyIiwieCI6NzkwMjgzLjU4LCJ5Ijo2MzE5MzA4LjEzLCJjaXR5IjoiQWlncmVtb250IiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC40MTkyNCwic3RyZWV0IjoiQ2hlbWluIGR1IEdyw6lzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuNTg0NDYxLDQ0LjEwMjIyNV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiQ2hlbWluIGR1IEdyw6hzIDMwMzMwIFRyZXNxdWVzIiwic2NvcmUiOjAuMzk3MjM2MzYzNjM2MzYzNiwiaWQiOiIzMDMzMV8wMDgzIiwibmFtZSI6IkNoZW1pbiBkdSBHcsOocyIsInBvc3Rjb2RlIjoiMzAzMzAiLCJjaXR5Y29kZSI6IjMwMzMxIiwieCI6ODI2ODQ3LjE2LCJ5Ijo2MzM0OTYzLjk2LCJjaXR5IjoiVHJlc3F1ZXMiLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQwOTYsInN0cmVldCI6IkNoZW1pbiBkdSBHcsOocyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0LjE1Njk3Nyw0NC4wMjg3OTFdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IkNoZW1pbiBkdSBHcsOpcyAzMDM2MCBOZXJzIiwic2NvcmUiOjAuMzk1Nzc5OTk5OTk5OTk5OTcsImlkIjoiMzAxODhfMDI2MCIsIm5hbWUiOiJDaGVtaW4gZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzYwIiwiY2l0eWNvZGUiOiIzMDE4OCIsIngiOjc5Mjc0Ni41MywieSI6NjMyNjIxMS42MywiY2l0eSI6Ik5lcnMiLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjM5MzU4LCJzdHJlZXQiOiJDaGVtaW4gZHUgR3LDqXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNC4xMjY5NjEsNDQuMDA2ODhdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IkNoZW1pbiBkdSBHcsOpcyAzMDM1MCBNYXJ1w6lqb2xzLWzDqHMtR2FyZG9uIiwic2NvcmUiOjAuMzg5MjgxODE4MTgxODE4MiwiaWQiOiIzMDE2MF8wMDQ2IiwibmFtZSI6IkNoZW1pbiBkdSBHcsOpcyIsInBvc3Rjb2RlIjoiMzAzNTAiLCJjaXR5Y29kZSI6IjMwMTYwIiwieCI6NzkwMzc1LjI3LCJ5Ijo2MzIzNzQyLjUyLCJjaXR5IjoiTWFydcOpam9scy1sw6hzLUdhcmRvbiIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMzIyMSwic3RyZWV0IjoiQ2hlbWluIGR1IEdyw6lzIn19XSwiYXR0cmlidXRpb24iOiJCQU4iLCJsaWNlbmNlIjoiRVRBTEFCLTIuMCIsInF1ZXJ5IjoiNzggUnVlIGR1IEdyw6lzIDMwMyIsImxpbWl0IjoxMH0= - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%2030310 - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '561' - Vary: - - Origin - Etag: - - W/"231-jbqSGt6/x4K0FWGGWwu4WBzdAD8" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuOTU5NTA3MjcyNzI3MjcyNywiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX1dLCJhdHRyaWJ1dGlvbiI6IkJBTiIsImxpY2VuY2UiOiJFVEFMQUItMi4wIiwicXVlcnkiOiI3OCBSdWUgZHUgR3LDqXMgMzAzMTAiLCJsaW1pdCI6MTB9 - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%2030310%20V - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4141' - Vary: - - Origin - Etag: - - W/"102d-/V1fRUVlD/3rJBcID+VimJZ4k6w" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuODY4NTk4MTgxODE4MTgxOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNC41NTQzMzQsNDMuOTcxNTI1XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3LDqXMgMzAyMTAgQ2FzdGlsbG9uLWR1LUdhcmQiLCJzY29yZSI6MC41MzQxMDgyNjA4Njk1NjUyLCJpZCI6IjMwMDczXzAwNjkiLCJuYW1lIjoiUnVlIGR1IEdyw6lzIiwicG9zdGNvZGUiOiIzMDIxMCIsImNpdHljb2RlIjoiMzAwNzMiLCJ4Ijo4MjQ3MjEuNDQsInkiOjYzMjAzOTYuNzcsImNpdHkiOiJDYXN0aWxsb24tZHUtR2FyZCIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMzk2OTMsInN0cmVldCI6IlJ1ZSBkdSBHcsOpcyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0LjQ3NDk0Niw0NC4wOTUyMDldfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkdSBHcsOocyAzMDMzMCBMYSBCYXN0aWRlLWQnRW5ncmFzIiwic2NvcmUiOjAuNTI0NTQwOTg4MTQyMjkyNiwiaWQiOiIzMDAzMV8wMDQ4IiwibmFtZSI6IlJ1ZSBkdSBHcsOocyIsInBvc3Rjb2RlIjoiMzAzMzAiLCJjaXR5Y29kZSI6IjMwMDMxIiwieCI6ODE4MDk1LjM0LCJ5Ijo2MzM0MDE0LjczLCJjaXR5IjoiTGEgQmFzdGlkZS1kJ0VuZ3JhcyIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuMjkxNjksInN0cmVldCI6IlJ1ZSBkdSBHcsOocyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlsxLjAwNDM1OCw0OS40NTk0MjNdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBkdSBHcsOpIDc2MzgwIE1vbnRpZ255Iiwic2NvcmUiOjAuNTA5NzI5NzQwMjU5NzQwMiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzY0NDZfMDIwMF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqSIsInBvc3Rjb2RlIjoiNzYzODAiLCJjaXR5Y29kZSI6Ijc2NDQ2IiwieCI6NTU1MjgyLjI3LCJ5Ijo2OTMwNzE5LjU0LCJjaXR5IjoiTW9udGlnbnkiLCJjb250ZXh0IjoiNzYsIFNlaW5lLU1hcml0aW1lLCBOb3JtYW5kaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjQ2NDE3LCJzdHJlZXQiOiJSdWUgZHUgR3LDqSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls0Ljc3MDYxOSw0My45NTgxOTRdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkdSBHcsOocyAzMDEzMyBMZXMgQW5nbGVzIiwic2NvcmUiOjAuNDkzMTU5MDkwOTA5MDkwOSwiaWQiOiIzMDAxMV9rZXR5dmEiLCJuYW1lIjoiUnVlIGR1IEdyw6hzIiwicG9zdGNvZGUiOiIzMDEzMyIsImNpdHljb2RlIjoiMzAwMTEiLCJ4Ijo4NDIxMDYuODYsInkiOjYzMTkyODEuMjYsImNpdHkiOiJMZXMgQW5nbGVzIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC41NDk3NSwic3RyZWV0IjoiUnVlIGR1IEdyw6hzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjk1ODQ2LDQzLjkzMTQ1OV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEdyw6lzIDMwMTkwIERpb25zIiwic2NvcmUiOjAuNDgxOTQ5MDkwOTA5MDkwOCwiaWQiOiIzMDEwMl8wMDg3IiwibmFtZSI6IlJ1ZSBkdSBHcsOpcyIsInBvc3Rjb2RlIjoiMzAxOTAiLCJjaXR5Y29kZSI6IjMwMTAyIiwieCI6ODA0MDU1LjE5LCJ5Ijo2MzE1NTcxLjA4LCJjaXR5IjoiRGlvbnMiLCJjb250ZXh0IjoiMzAsIEdhcmQsIE9jY2l0YW5pZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjQyNjQ0LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMy45NDA3NjMsNDMuODcxNzExXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3LDqHMgMzAyNjAgQ29yY29ubmUiLCJzY29yZSI6MC40NzA1MDU0NTQ1NDU0NTQ1LCJ0eXBlIjoibG9jYWxpdHkiLCJpbXBvcnRhbmNlIjowLjMwMDU2LCJpZCI6IjMwMDk1XzAwNjAiLCJuYW1lIjoiUnVlIGR1IEdyw6hzIiwicG9zdGNvZGUiOiIzMDI2MCIsImNpdHljb2RlIjoiMzAwOTUiLCJ4Ijo3NzU2MjMuMDYsInkiOjYzMDg1MjguNjIsImNpdHkiOiJDb3Jjb25uZSIsImNvbnRleHQiOiIzMCwgR2FyZCwgT2NjaXRhbmllIiwibG9jYWxpdHkiOiJSdWUgZHUgR3LDqHMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi40OTcyNDgsNDYuNjAxNzM0XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3JlcyBSb3NlIDE4MzYwIFNhdWx6YWlzLWxlLVBvdGllciIsInNjb3JlIjowLjQ1MTg1MDkwOTA5MDkwOTEsImlkIjoiMTgyNDVfMDAxOCIsIm5hbWUiOiJSdWUgZHUgR3JlcyBSb3NlIiwicG9zdGNvZGUiOiIxODM2MCIsImNpdHljb2RlIjoiMTgyNDUiLCJ4Ijo2NjE1MTYuMjcsInkiOjY2MTE0MjAuNzUsImNpdHkiOiJTYXVsemFpcy1sZS1Qb3RpZXIiLCJjb250ZXh0IjoiMTgsIENoZXIsIENlbnRyZS1WYWwgZGUgTG9pcmUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC40NzAzNiwic3RyZWV0IjoiUnVlIGR1IEdyZXMgUm9zZSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstNTIuNDc5NDMzLDQuOTg5OTkyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR1LDiFMgUk9VR0UgOTczNTUgTWFjb3VyaWEiLCJzY29yZSI6MC40NDgxNDYzNjM2MzYzNjM1NiwiaWQiOiI5NzMwNV8wNzU4IiwibmFtZSI6IlJ1ZSBkdSBHUsOIUyBST1VHRSIsInBvc3Rjb2RlIjoiOTczNTUiLCJjaXR5Y29kZSI6Ijk3MzA1IiwieCI6MzM1OTc3Ljg2LCJ5Ijo1NTE3NDIuMjUsImNpdHkiOiJNYWNvdXJpYSIsImNvbnRleHQiOiI5NzMsIEd1eWFuZSIsInR5cGUiOiJzdHJlZXQiLCJpbXBvcnRhbmNlIjowLjYwOTYxLCJzdHJlZXQiOiJSdWUgZHUgR1LDiFMgUk9VR0UifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi40MzIxNDUsNDYuNTQwNzk2XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR3JlcyBSb3NlIDE4MzYwIFZlc2R1biIsInNjb3JlIjowLjQ0NDQ1MzYzNjM2MzYzNjMzLCJpZCI6IjE4Mjc4XzAwMzQiLCJuYW1lIjoiUnVlIGR1IEdyZXMgUm9zZSIsInBvc3Rjb2RlIjoiMTgzNjAiLCJjaXR5Y29kZSI6IjE4Mjc4IiwieCI6NjU2NDg0LjMsInkiOjY2MDQ2ODcuMTEsImNpdHkiOiJWZXNkdW4iLCJjb250ZXh0IjoiMTgsIENoZXIsIENlbnRyZS1WYWwgZGUgTG9pcmUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC4zODg5OSwic3RyZWV0IjoiUnVlIGR1IEdyZXMgUm9zZSJ9fV0sImF0dHJpYnV0aW9uIjoiQkFOIiwibGljZW5jZSI6IkVUQUxBQi0yLjAiLCJxdWVyeSI6Ijc4IFJ1ZSBkdSBHcsOpcyAzMDMxMCBWIiwibGltaXQiOjEwfQ== - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT + W3sibm9tIjoiQXBwaWxseSIsImNvZGUiOiI2MDAyMSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDAyMTYiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1MTd9LHsibm9tIjoiQmFixZN1ZiIsImNvZGUiOiI2MDAzNyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDAzNjQiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1MTF9LHsibm9tIjoiQmVhdXJhaW5zLWzDqHMtTm95b24iLCJjb2RlIjoiNjAwNTUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAwNTQ3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MzM4fSx7Im5vbSI6IkLDqWjDqXJpY291cnQiLCJjb2RlIjoiNjAwNTkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAwNTg4IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MTk4fSx7Im5vbSI6IkJyw6l0aWdueSIsImNvZGUiOiI2MDEwNSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDEwNTciLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo0MzF9LHsibm9tIjoiQnVzc3kiLCJjb2RlIjoiNjAxMTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAxMTcyIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MzA0fSx7Im5vbSI6IkNhaXNuZXMiLCJjb2RlIjoiNjAxMTgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAxMTgwIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NTA3fSx7Im5vbSI6IkNyaXNvbGxlcyIsImNvZGUiOiI2MDE4MSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDE4MDAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo5MTR9LHsibm9tIjoiQ3V0cyIsImNvZGUiOiI2MDE4OSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDE4ODMiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo5NjF9LHsibm9tIjoiR2VudnJ5IiwiY29kZSI6IjYwMjcwIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwMjY3NSIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjMzMn0seyJub20iOiJHcmFuZHLDuyIsImNvZGUiOiI2MDI4NyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDI4NDAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozNjR9LHsibm9tIjoiTGFyYnJveWUiLCJjb2RlIjoiNjAzNDgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDAzNDY3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NTE2fSx7Im5vbSI6Ik1vbmRlc2NvdXJ0IiwiY29kZSI6IjYwNDEwIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNDA2OSIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjI0OH0seyJub20iOiJNb3JsaW5jb3VydCIsImNvZGUiOiI2MDQzMSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDQyNjciLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo1MjZ9LHsibm9tIjoiTmFtcGNlbCIsImNvZGUiOiI2MDQ0NSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDQ0MDgiLCJjb2RlRXBjaSI6IjI0NjAwMDc0OSIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozMDR9LHsibm9tIjoiTm95b24iLCJjb2RlIjoiNjA0NzEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA0NjU1IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6MTI5ODd9LHsibm9tIjoiUGFzc2VsIiwiY29kZSI6IjYwNDg4IiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNDgyMCIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjI3MH0seyJub20iOiJQb250LWwnw4l2w6pxdWUiLCJjb2RlIjoiNjA1MDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA1MDA5IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6Njc1fSx7Im5vbSI6IlBvbnRvaXNlLWzDqHMtTm95b24iLCJjb2RlIjoiNjA1MDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA1MDE3IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NDUwfSx7Im5vbSI6IlBvcnF1w6lyaWNvdXJ0IiwiY29kZSI6IjYwNTExIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNTA1OCIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjQxMH0seyJub20iOiJTYWxlbmN5IiwiY29kZSI6IjYwNjAzIiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNTk2NyIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjg5Mn0seyJub20iOiJTZW1waWdueSIsImNvZGUiOiI2MDYxMCIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDYwMTUiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjo3NjB9LHsibm9tIjoiU2VybWFpemUiLCJjb2RlIjoiNjA2MTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA2MDgwIiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6Mjc0fSx7Im5vbSI6IlN1em95IiwiY29kZSI6IjYwNjI1IiwiY29kZURlcGFydGVtZW50IjoiNjAiLCJzaXJlbiI6IjIxNjAwNjE2MyIsImNvZGVFcGNpIjoiMjQ2MDAwNzU2IiwiY29kZVJlZ2lvbiI6IjMyIiwiY29kZXNQb3N0YXV4IjpbIjYwNDAwIl0sInBvcHVsYXRpb24iOjU1NX0seyJub20iOiJWYXJlc25lcyIsImNvZGUiOiI2MDY1NSIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDY0NjAiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjozNjN9LHsibm9tIjoiVmF1Y2hlbGxlcyIsImNvZGUiOiI2MDY1NyIsImNvZGVEZXBhcnRlbWVudCI6IjYwIiwic2lyZW4iOiIyMTYwMDY0ODYiLCJjb2RlRXBjaSI6IjI0NjAwMDc1NiIsImNvZGVSZWdpb24iOiIzMiIsImNvZGVzUG9zdGF1eCI6WyI2MDQwMCJdLCJwb3B1bGF0aW9uIjoyNDN9LHsibm9tIjoiVmlsbGUiLCJjb2RlIjoiNjA2NzYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiI2MCIsInNpcmVuIjoiMjE2MDA2Njc2IiwiY29kZUVwY2kiOiIyNDYwMDA3NTYiLCJjb2RlUmVnaW9uIjoiMzIiLCJjb2Rlc1Bvc3RhdXgiOlsiNjA0MDAiXSwicG9wdWxhdGlvbiI6NzUwfV0= + recorded_at: Tue, 02 Jul 2024 13:53:51 GMT - request: method: get uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%2030310%20Verg%C3%A8 @@ -423,9 +55,9 @@ http_interactions: message: '' headers: Server: - - nginx/1.25.3 + - nginx/1.25.5 Date: - - Mon, 04 Mar 2024 09:41:12 GMT + - Tue, 02 Jul 2024 13:53:52 GMT Content-Type: - application/json; charset=utf-8 Content-Length: @@ -433,19 +65,19 @@ http_interactions: Vary: - Origin Etag: - - W/"238-PKhw2BRdtojt7kPtuUxNTXlI3i8" + - W/"238-Y47qALrriF7wD0KtsLJdCgH+fmc" X-Cache-Status: - - MISS + - HIT Access-Control-Allow-Headers: - X-Requested-With,Content-Type body: encoding: ASCII-8BIT string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuODY4NTk4MTgxODE4MTgxOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX1dLCJhdHRyaWJ1dGlvbiI6IkJBTiIsImxpY2VuY2UiOiJFVEFMQUItMi4wIiwicXVlcnkiOiI3OCBSdWUgZHUgR3LDqXMgMzAzMTAgVmVyZ8OoIiwibGltaXQiOjEwfQ== - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT + eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuODY4NzI2MzYzNjM2MzYzNiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NTk5LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX1dLCJhdHRyaWJ1dGlvbiI6IkJBTiIsImxpY2VuY2UiOiJFVEFMQUItMi4wIiwicXVlcnkiOiI3OCBSdWUgZHUgR3LDqXMgMzAzMTAgVmVyZ8OoIiwibGltaXQiOjEwfQ== + recorded_at: Tue, 02 Jul 2024 13:53:52 GMT - request: method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr%C3%A9s%2030310%20Ver + uri: https://data.education.gouv.fr/api/records/1.0/search?dataset=fr-en-annuaire-education&q=Moulin&rows=5 body: encoding: US-ASCII string: '' @@ -460,98 +92,175 @@ http_interactions: message: '' headers: Server: - - nginx/1.25.3 + - openresty Date: - - Mon, 04 Mar 2024 09:41:12 GMT + - Tue, 02 Jul 2024 13:54:04 GMT Content-Type: - application/json; charset=utf-8 Content-Length: - - '565' + - '10143' + X-Ratelimit-Remaining: + - '4927' + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Reset: + - '2024-07-03 00:00:00+00:00' + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate Vary: - - Origin - Etag: - - W/"235-DA17b0DXZrDgQoRCces7jzDyzFY" - X-Cache-Status: - - MISS + - Accept-Language, Cookie, Host + Content-Language: + - fr-fr + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Methods: + - POST, GET, OPTIONS + Access-Control-Max-Age: + - '1000' Access-Control-Allow-Headers: - - X-Requested-With,Content-Type + - Authorization, X-Requested-With, Origin, ODS-API-Analytics-App, ODS-API-Analytics-Embed-Type, + ODS-API-Analytics-Embed-Referrer, ODS-Widgets-Version, Accept + Access-Control-Expose-Headers: + - ODS-Explore-API-Deprecation, Link, X-RateLimit-Remaining, X-RateLimit-Limit, + X-RateLimit-Reset, X-RateLimit-dataset-Remaining, X-RateLimit-dataset-Limit, + X-RateLimit-dataset-Reset + Strict-Transport-Security: + - max-age=31536000 + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + Referrer-Policy: + - strict-origin-when-cross-origin + Permissions-Policy: + - midi=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=() + Content-Security-Policy: + - upgrade-insecure-requests; + X-Ua-Compatible: + - IE=edge body: encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzQuMjMwNzQ3LDQzLjc0NjA2NF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IEdyw6lzIDMwMzEwIFZlcmfDqHplIiwic2NvcmUiOjAuODY4NTk4MTgxODE4MTgxOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzAzNDRfMDA5OF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgR3LDqXMiLCJwb3N0Y29kZSI6IjMwMzEwIiwiY2l0eWNvZGUiOiIzMDM0NCIsIngiOjc5OTE0OS4zMywieSI6NjI5NDg4OC4zMSwiY2l0eSI6IlZlcmfDqHplIiwiY29udGV4dCI6IjMwLCBHYXJkLCBPY2NpdGFuaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjU1NDU4LCJzdHJlZXQiOiJSdWUgZHUgR3LDqXMifX1dLCJhdHRyaWJ1dGlvbiI6IkJBTiIsImxpY2VuY2UiOiJFVEFMQUItMi4wIiwicXVlcnkiOiI3OCBSdWUgZHUgR3LDqXMgMzAzMTAgVmVyIiwibGltaXQiOjEwfQ== - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20 - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:12 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4416' - Vary: - - Origin - Etag: - - W/"1140-7druqEPKyu54Y61r7AheVXwHMF0" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6Wy0wLjU4NDA5Myw0NC44MzEwNDNdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IFJ1ZSBkdSBUb25kdSAzMzAwMCBCb3JkZWF1eCIsInNjb3JlIjowLjg5NDYxNzI3MjcyNzI3MjYsImhvdXNlbnVtYmVyIjoiNzgiLCJpZCI6IjMzMDYzXzg5NzVfMDAwNzgiLCJuYW1lIjoiNzggUnVlIGR1IFRvbmR1IiwicG9zdGNvZGUiOiIzMzAwMCIsImNpdHljb2RlIjoiMzMwNjMiLCJ4Ijo0MTY4MjEuMiwieSI6NjQyMTA4MS4xNSwiY2l0eSI6IkJvcmRlYXV4IiwiY29udGV4dCI6IjMzLCBHaXJvbmRlLCBOb3V2ZWxsZS1BcXVpdGFpbmUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjg0MDc5LCJzdHJlZXQiOiJSdWUgZHUgVG9uZHUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS40NDMzODMsNDMuNTc4ODYzXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgRsOpcsOpdHJhIDMxNDAwIFRvdWxvdXNlIiwic2NvcmUiOjAuODkyNTYxODE4MTgxODE4MiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMzE1NTVfMzI1Nl8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgRsOpcsOpdHJhIiwicG9zdGNvZGUiOiIzMTQwMCIsImNpdHljb2RlIjoiMzE1NTUiLCJ4Ijo1NzQyMzUuMjUsInkiOjYyNzY3NzMuMjgsImNpdHkiOiJUb3Vsb3VzZSIsImNvbnRleHQiOiIzMSwgSGF1dGUtR2Fyb25uZSwgT2NjaXRhbmllIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44MTgxOCwic3RyZWV0IjoiUnVlIGR1IEbDqXLDqXRyYSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlstMS41MjM1OTcsNDcuMjM2NTk0XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgQ3JvaXNzYW50IDQ0MzAwIE5hbnRlcyIsInNjb3JlIjowLjg5MjM0NTQ1NDU0NTQ1NDQsImhvdXNlbnVtYmVyIjoiNzgiLCJpZCI6IjQ0MTA5XzIzMTJfMDAwNzgiLCJuYW1lIjoiNzggUnVlIGR1IENyb2lzc2FudCIsInBvc3Rjb2RlIjoiNDQzMDAiLCJjaXR5Y29kZSI6IjQ0MTA5IiwieCI6MzU3OTYwLjM3LCJ5Ijo2NjkxNjEwLjMxLCJjaXR5IjoiTmFudGVzIiwiY29udGV4dCI6IjQ0LCBMb2lyZS1BdGxhbnRpcXVlLCBQYXlzIGRlIGxhIExvaXJlIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44MTU4LCJzdHJlZXQiOiJSdWUgZHUgQ3JvaXNzYW50In19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzIuMzczOTMxLDQ4LjgzMTM5OV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IENoZXZhbGVyZXQgNzUwMTMgUGFyaXMiLCJzY29yZSI6MC44OTIyMiwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzUxMTNfMTk5MF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgQ2hldmFsZXJldCIsInBvc3Rjb2RlIjoiNzUwMTMiLCJjaXR5Y29kZSI6Ijc1MTEzIiwieCI6NjU0MDQxLjI0LCJ5Ijo2ODU5MjIwLjI5LCJjaXR5IjoiUGFyaXMiLCJkaXN0cmljdCI6IlBhcmlzIDEzZSBBcnJvbmRpc3NlbWVudCIsImNvbnRleHQiOiI3NSwgUGFyaXMsIMOObGUtZGUtRnJhbmNlIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44MTQ0Miwic3RyZWV0IjoiUnVlIGR1IENoZXZhbGVyZXQifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNC4wNDAzMzMsNDkuMjQ4NDUzXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBydWUgZHUgQmFyYsOidHJlIDUxMTAwIFJlaW1zIiwic2NvcmUiOjAuODkyMjEzNjM2MzYzNjM2NCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNTE0NTRfMDYwMF8wMDA3OCIsIm5hbWUiOiI3OCBydWUgZHUgQmFyYsOidHJlIiwicG9zdGNvZGUiOiI1MTEwMCIsImNpdHljb2RlIjoiNTE0NTQiLCJ4Ijo3NzU3NTYuNzYsInkiOjY5MDU5MTkuNDksImNpdHkiOiJSZWltcyIsImNvbnRleHQiOiI1MSwgTWFybmUsIEdyYW5kIEVzdCIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODE0MzUsInN0cmVldCI6InJ1ZSBkdSBCYXJiw6J0cmUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi4yOTE2NjMsNDguODQ3NDU4XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgVGjDqcOidHJlIDc1MDE1IFBhcmlzIiwic2NvcmUiOjAuODkyMTAwOTA5MDkwOTA5MSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzUxMTVfOTIzMF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgVGjDqcOidHJlIiwicG9zdGNvZGUiOiI3NTAxNSIsImNpdHljb2RlIjoiNzUxMTUiLCJ4Ijo2NDgwMTguMjQsInkiOjY4NjEwNTYuOSwiY2l0eSI6IlBhcmlzIiwiZGlzdHJpY3QiOiJQYXJpcyAxNWUgQXJyb25kaXNzZW1lbnQiLCJjb250ZXh0IjoiNzUsIFBhcmlzLCDDjmxlLWRlLUZyYW5jZSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuODEzMTEsInN0cmVldCI6IlJ1ZSBkdSBUaMOpw6J0cmUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMy4xNDE0OTQsNTAuNzIzNDAyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgQ2xpbnF1ZXQgNTkyMDAgVG91cmNvaW5nIiwic2NvcmUiOjAuODkxNzg1NDU0NTQ1NDU0NSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNTk1OTlfMTMzMF8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgQ2xpbnF1ZXQiLCJwb3N0Y29kZSI6IjU5MjAwIiwiY2l0eWNvZGUiOiI1OTU5OSIsIngiOjcxMDAwOS41OSwieSI6NzA2OTY0MS42NSwiY2l0eSI6IlRvdXJjb2luZyIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC44MDk2NCwic3RyZWV0IjoiUnVlIGR1IENsaW5xdWV0In19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzMuMDkwMjkxLDUwLjY0ODUxOF19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IEJvaXMgNTk4MDAgTGlsbGUiLCJzY29yZSI6MC4zODkyNzUzMTQ2ODUzMTQ3LCJpZCI6IjU5MzUwXzA5MzQiLCJuYW1lIjoiUnVlIGR1IEJvaXMiLCJwb3N0Y29kZSI6IjU5ODAwIiwiY2l0eWNvZGUiOiI1OTM1MCIsIm9sZGNpdHljb2RlIjoiNTkzNTAiLCJ4Ijo3MDYzOTYuOTMsInkiOjcwNjEyOTEuMzksImNpdHkiOiJMaWxsZSIsIm9sZGNpdHkiOiJMaWxsZSIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuODIwNDksInN0cmVldCI6IlJ1ZSBkdSBCb2lzIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuNDQyMzk5LDQzLjYwNjM4OV19LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiUnVlIGR1IFRhdXIgMzEwMDAgVG91bG91c2UiLCJzY29yZSI6MC4zODg2OTYyMjM3NzYyMjM3NiwiaWQiOiIzMTU1NV84NDA4IiwibmFtZSI6IlJ1ZSBkdSBUYXVyIiwicG9zdGNvZGUiOiIzMTAwMCIsImNpdHljb2RlIjoiMzE1NTUiLCJ4Ijo1NzQyMTYuMSwieSI6NjI3OTgzMy41MiwiY2l0eSI6IlRvdWxvdXNlIiwiY29udGV4dCI6IjMxLCBIYXV0ZS1HYXJvbm5lLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC44MTQxMiwic3RyZWV0IjoiUnVlIGR1IFRhdXIifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMS40MTc5NDMsNDMuNTc4MjA3XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgQ2FnaXJlIDMxMTAwIFRvdWxvdXNlIiwic2NvcmUiOjAuMzQ2NzM4MTgxODE4MTgxOCwiaWQiOiIzMTU1NV8xNDk2IiwibmFtZSI6IlJ1ZSBkdSBDYWdpcmUiLCJwb3N0Y29kZSI6IjMxMTAwIiwiY2l0eWNvZGUiOiIzMTU1NSIsIngiOjU3MjE3OC42NywieSI6NjI3Njc0MS4yNCwiY2l0eSI6IlRvdWxvdXNlIiwiY29udGV4dCI6IjMxLCBIYXV0ZS1HYXJvbm5lLCBPY2NpdGFuaWUiLCJ0eXBlIjoic3RyZWV0IiwiaW1wb3J0YW5jZSI6MC44MTQxMiwic3RyZWV0IjoiUnVlIGR1IENhZ2lyZSJ9fV0sImF0dHJpYnV0aW9uIjoiQkFOIiwibGljZW5jZSI6IkVUQUxBQi0yLjAiLCJxdWVyeSI6Ijc4IFJ1ZSBkdSAiLCJsaW1pdCI6MTB9 - recorded_at: Mon, 04 Mar 2024 09:41:12 GMT -- request: - method: get - uri: https://api-adresse.data.gouv.fr/search?limit=10&q=78%20Rue%20du%20Gr - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - demarches-simplifiees.fr - Expect: - - '' - response: - status: - code: 200 - message: '' - headers: - Server: - - nginx/1.25.3 - Date: - - Mon, 04 Mar 2024 09:41:13 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '4411' - Vary: - - Origin - Etag: - - W/"113b-Bi9ByBNLSvhokhk7dJmhHV5RY/U" - X-Cache-Status: - - MISS - Access-Control-Allow-Headers: - - X-Requested-With,Content-Type - body: - encoding: ASCII-8BIT - string: !binary |- - eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJ2ZXJzaW9uIjoiZHJhZnQiLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzUuNjM3OTgyLDQzLjYzNzE5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgR3JhbmRlIDEzNDkwIEpvdXF1ZXMiLCJzY29yZSI6MC40OTMzNjk5OTk5OTk5OTk5LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIxMzA0OF8zMDUwXzAwMDc4IiwibmFtZSI6Ijc4IFJ1ZSBHcmFuZGUiLCJwb3N0Y29kZSI6IjEzNDkwIiwiY2l0eWNvZGUiOiIxMzA0OCIsIngiOjkxMjg4OS44LCJ5Ijo2Mjg1NTcyLjUxLCJjaXR5IjoiSm91cXVlcyIsImNvbnRleHQiOiIxMywgQm91Y2hlcy1kdS1SaMO0bmUsIFByb3ZlbmNlLUFscGVzLUPDtHRlIGQnQXp1ciIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuNjI3MDcsInN0cmVldCI6IlJ1ZSBHcmFuZGUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMy41NTU4ODIsNDguNTkyNzQxXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBSdWUgZHUgUGVycmV5IDEwMzcwIFZpbGxlbmF1eGUtbGEtR3JhbmRlIiwic2NvcmUiOjAuNDg0NzA1OTg5MzA0ODEyOCwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiMTA0MjBfMDY0MV8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgUGVycmV5IiwicG9zdGNvZGUiOiIxMDM3MCIsImNpdHljb2RlIjoiMTA0MjAiLCJ4Ijo3NDA5OTMuMzMsInkiOjY4MzI2NDguMTUsImNpdHkiOiJWaWxsZW5hdXhlLWxhLUdyYW5kZSIsImNvbnRleHQiOiIxMCwgQXViZSwgR3JhbmQgRXN0IiwidHlwZSI6ImhvdXNlbnVtYmVyIiwiaW1wb3J0YW5jZSI6MC41NjcwNiwic3RyZWV0IjoiUnVlIGR1IFBlcnJleSJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls1LjU1MDIwNiw0Ni4xMzUxMDJdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IEdyYW5kZSBSdWUgMDE0MzAgU2FpbnQtTWFydGluLWR1LUZyw6puZSIsInNjb3JlIjowLjQ4NDA2MzYzNjM2MzYzNjMsImhvdXNlbnVtYmVyIjoiNzgiLCJpZCI6IjAxMzczXzAxNTBfMDAwNzgiLCJuYW1lIjoiNzggR3JhbmRlIFJ1ZSIsInBvc3Rjb2RlIjoiMDE0MzAiLCJjaXR5Y29kZSI6IjAxMzczIiwieCI6ODk2ODQ5LjE5LCJ5Ijo2NTYyNjU2LjA2LCJjaXR5IjoiU2FpbnQtTWFydGluLWR1LUZyw6puZSIsImNvbnRleHQiOiIwMSwgQWluLCBBdXZlcmduZS1SaMO0bmUtQWxwZXMiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjUyNDcsInN0cmVldCI6IkdyYW5kZSBSdWUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNS45MDE3NjEsNDYuNTk2Mjg4XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiI3OCBHcmFuZGUgUnVlIDM5MTUwIExhIENoYXV4LWR1LURvbWJpZWYiLCJzY29yZSI6MC40Nzk3NjE4MTgxODE4MTgyLCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzOTEzMV8wMDI1XzAwMDc4IiwibmFtZSI6Ijc4IEdyYW5kZSBSdWUiLCJwb3N0Y29kZSI6IjM5MTUwIiwiY2l0eWNvZGUiOiIzOTEzMSIsIngiOjkyMjA5Mi4zNywieSI6NjYxNDc3NC42NywiY2l0eSI6IkxhIENoYXV4LWR1LURvbWJpZWYiLCJjb250ZXh0IjoiMzksIEp1cmEsIEJvdXJnb2duZS1GcmFuY2hlLUNvbXTDqSIsInR5cGUiOiJob3VzZW51bWJlciIsImltcG9ydGFuY2UiOjAuNDc3MzgsInN0cmVldCI6IkdyYW5kZSBSdWUifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbNS45ODUyOCw0Ni42MTc5MzddfSwicHJvcGVydGllcyI6eyJsYWJlbCI6Ijc4IEdyYW5kZSBSdWUgMzkxNTAgRm9ydC1kdS1QbGFzbmUiLCJzY29yZSI6MC40Nzc1ODcyNzI3MjcyNzI3LCJob3VzZW51bWJlciI6Ijc4IiwiaWQiOiIzOTIzMl8wMDIwXzAwMDc4IiwibmFtZSI6Ijc4IEdyYW5kZSBSdWUiLCJwb3N0Y29kZSI6IjM5MTUwIiwiY2l0eWNvZGUiOiIzOTIzMiIsIngiOjkyODM5MC43OSwieSI6NjYxNzQxNS41MywiY2l0eSI6IkZvcnQtZHUtUGxhc25lIiwiY29udGV4dCI6IjM5LCBKdXJhLCBCb3VyZ29nbmUtRnJhbmNoZS1Db210w6kiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjQ1MzQ2LCJzdHJlZXQiOiJHcmFuZGUgUnVlIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuMjI2NzQ4LDQ5Ljc4NTM3N119LCJwcm9wZXJ0aWVzIjp7ImxhYmVsIjoiNzggUnVlIGR1IENpbWV0aWVyZSA3Njk1MCBMZXMgR3JhbmRlcy1WZW50ZXMiLCJzY29yZSI6MC40MTEyMjQ1NDU0NTQ1NDU0NSwiaG91c2VudW1iZXIiOiI3OCIsImlkIjoiNzYzMjFfMDA0MV8wMDA3OCIsIm5hbWUiOiI3OCBSdWUgZHUgQ2ltZXRpZXJlIiwicG9zdGNvZGUiOiI3Njk1MCIsImNpdHljb2RlIjoiNzYzMjEiLCJ4Ijo1NzIyMjAuOTMsInkiOjY5NjY1OTguNDMsImNpdHkiOiJMZXMgR3JhbmRlcy1WZW50ZXMiLCJjb250ZXh0IjoiNzYsIFNlaW5lLU1hcml0aW1lLCBOb3JtYW5kaWUiLCJ0eXBlIjoiaG91c2VudW1iZXIiLCJpbXBvcnRhbmNlIjowLjQ3MzQ3LCJzdHJlZXQiOiJSdWUgZHUgQ2ltZXRpZXJlIn19LHsidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9pbnQiLCJjb29yZGluYXRlcyI6WzEuMjA3NDA0LDQ5Ljc3NjYyXX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgR291bGV0IDc2OTUwIExlcyBHcmFuZGVzLVZlbnRlcyIsInNjb3JlIjowLjQwMTc1NzI3MjcyNzI3Mjc0LCJpZCI6Ijc2MzIxXzAwNTUiLCJuYW1lIjoiUnVlIGR1IEdvdWxldCIsInBvc3Rjb2RlIjoiNzY5NTAiLCJjaXR5Y29kZSI6Ijc2MzIxIiwieCI6NTcwODA1LjEzLCJ5Ijo2OTY1NjU1LjQ3LCJjaXR5IjoiTGVzIEdyYW5kZXMtVmVudGVzIiwiY29udGV4dCI6Ijc2LCBTZWluZS1NYXJpdGltZSwgTm9ybWFuZGllIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNDgxODMsInN0cmVldCI6IlJ1ZSBkdSBHb3VsZXQifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi4yODk1MTQsNTEuMDA0NzQ5XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgTGFjIDU5NzYwIEdyYW5kZS1TeW50aGUiLCJzY29yZSI6MC40MDAyNjY2MjMzNzY2MjMzLCJpZCI6IjU5MjcxXzAzOTciLCJuYW1lIjoiUnVlIGR1IExhYyIsInBvc3Rjb2RlIjoiNTk3NjAiLCJjaXR5Y29kZSI6IjU5MjcxIiwieCI6NjUwMDIxLjUxLCJ5Ijo3MTAxMjE4LjkxLCJjaXR5IjoiR3JhbmRlLVN5bnRoZSIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNTQ1NzksInN0cmVldCI6IlJ1ZSBkdSBMYWMifX0seyJ0eXBlIjoiRmVhdHVyZSIsImdlb21ldHJ5Ijp7InR5cGUiOiJQb2ludCIsImNvb3JkaW5hdGVzIjpbMi4zNzM4NzksNTAuOTk3NTY2XX0sInByb3BlcnRpZXMiOnsibGFiZWwiOiJSdWUgZHUgTGFjIDU5MTgwIENhcHBlbGxlLWxhLUdyYW5kZSIsInNjb3JlIjowLjM5ODE0MDI1OTc0MDI1OTcsImlkIjoiNTkxMzFfMDM5NyIsIm5hbWUiOiJSdWUgZHUgTGFjIiwicG9zdGNvZGUiOiI1OTE4MCIsImNpdHljb2RlIjoiNTkxMzEiLCJ4Ijo2NTU5NDkuNiwieSI6NzEwMDM2Ny44NiwiY2l0eSI6IkNhcHBlbGxlLWxhLUdyYW5kZSIsImNvbnRleHQiOiI1OSwgTm9yZCwgSGF1dHMtZGUtRnJhbmNlIiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNTIyNCwic3RyZWV0IjoiUnVlIGR1IExhYyJ9fSx7InR5cGUiOiJGZWF0dXJlIiwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOls2LjEzMzgwNiw0OS40MTM5NDFdfSwicHJvcGVydGllcyI6eyJsYWJlbCI6IlJ1ZSBkdSBSZXliYWNoIDU3MzMwIEhldHRhbmdlLUdyYW5kZSIsInNjb3JlIjowLjM4Njg2NzQ4NjYzMTAxNjA2LCJpZCI6IjU3MzIzXzEyNjUiLCJuYW1lIjoiUnVlIGR1IFJleWJhY2giLCJwb3N0Y29kZSI6IjU3MzMwIiwiY2l0eWNvZGUiOiI1NzMyMyIsIngiOjkyNzQxOS4xNCwieSI6NjkyODM0My44MiwiY2l0eSI6IkhldHRhbmdlLUdyYW5kZSIsImNvbnRleHQiOiI1NywgTW9zZWxsZSwgR3JhbmQgRXN0IiwidHlwZSI6InN0cmVldCIsImltcG9ydGFuY2UiOjAuNTQ5NjYsInN0cmVldCI6IlJ1ZSBkdSBSZXliYWNoIn19XSwiYXR0cmlidXRpb24iOiJCQU4iLCJsaWNlbmNlIjoiRVRBTEFCLTIuMCIsInF1ZXJ5IjoiNzggUnVlIGR1IEdyIiwibGltaXQiOjEwfQ== - recorded_at: Mon, 04 Mar 2024 09:41:13 GMT + string: '{"nhits": 1230, "parameters": {"dataset": "fr-en-annuaire-education", + "q": "Moulin", "rows": 5, "start": 0, "format": "json", "timezone": "UTC"}, + "records": [{"datasetid": "fr-en-annuaire-education", "recordid": "5a1303829afed9e2f13d93bd0f56c4c526a0cd62", + "fields": {"position": [44.26226619871097, 5.960521626379292], "statut_public_prive": + "Priv\u00e9", "restauration": 0, "type_contrat_prive": "HORS CONTRAT", "ecole_maternelle": + 1, "libelle_departement": "Alpes-de-Haute-Provence", "hebergement": 0, "date_ouverture": + "2019-09-01", "code_region": "93", "libelle_bassin_formation": "DIGNE SISTERON", + "rpi_concentre": 0, "type_etablissement": "Ecole", "ministere_tutelle": "MINISTERE + DE L''EDUCATION NATIONALE", "precision_localisation": "Ville", "libelle_academie": + "Aix-Marseille", "telephone": "0492621133", "code_nature": 151, "code_type_contrat_prive": + "10", "longitude": 5.960521626379292, "etat": "OUVERT", "latitude": 44.26226619871097, + "code_academie": "02", "ecole_elementaire": 1, "adresse_1": "Lieu-dit le moulin", + "coordy_origine": 6355902.4, "code_commune": "04231", "code_circonscription": + "0134548Y", "identifiant_de_l_etablissement": "0040581K", "adresse_3": "04200 + VALERNES", "multi_uai": 0, "libelle_nature": "ECOLE DE NIVEAU ELEMENTAIRE", + "mail": "moulin@gdv-cor.org", "nom_circonscription": "CIRCONSCRIPTION 1er + D. ETABLISSEMENTS PRIVES Hors Contrat", "libelle_region": "Provence-Alpes-C\u00f4te + d''Azur", "ulis": 0, "nom_etablissement": "Communaut\u00e9 de la R\u00e9conciliation", + "nom_commune": "Valernes", "code_departement": "004", "code_bassin_formation": + "02101", "code_postal": "04200", "epsg_origine": "EPSG:2154", "date_maj_ligne": + "2024-06-29", "coordx_origine": 936304.3}, "geometry": {"type": "Point", "coordinates": + [5.960521626379292, 44.26226619871097]}, "record_timestamp": "2024-07-02T01:01:00Z"}, + {"datasetid": "fr-en-annuaire-education", "recordid": "1d41d58dd458e147633388e57acc0950e2d8b65f", + "fields": {"position": [46.56509840412523, 3.327179664489305], "statut_public_prive": + "Public", "restauration": 0, "type_contrat_prive": "SANS OBJET", "ecole_maternelle": + 1, "libelle_departement": "Allier", "hebergement": 0, "date_ouverture": "1967-06-06", + "siren_siret": "21030190900060", "code_region": "84", "libelle_bassin_formation": + "MOULINS", "rpi_concentre": 0, "type_etablissement": "Ecole", "ministere_tutelle": + "MINISTERE DE L''EDUCATION NATIONALE", "precision_localisation": "Num\u00e9ro + de rue", "libelle_academie": "Clermont-Ferrand", "telephone": "0470440367", + "code_nature": 151, "code_type_contrat_prive": "99", "longitude": 3.327179664489305, + "etat": "OUVERT", "latitude": 46.56509840412523, "code_academie": "06", "ecole_elementaire": + 1, "adresse_1": "25 rue Louis Blanc", "coordy_origine": 6607281.5, "code_commune": + "03190", "code_circonscription": "0030064D", "identifiant_de_l_etablissement": + "0030323K", "nombre_d_eleves": 294, "adresse_3": "03000 MOULINS", "multi_uai": + 0, "libelle_nature": "ECOLE DE NIVEAU ELEMENTAIRE", "mail": "ce.0030323K@ac-clermont.fr", + "nom_circonscription": "Circonscription d''inspection du 1er degr\u00e9 de + Moulins I", "libelle_region": "Auvergne-Rh\u00f4ne-Alpes", "ulis": 1, "nom_etablissement": + "Ecole primaire Jean Moulin", "pial": "0030013Y", "nom_commune": "Moulins", + "code_departement": "003", "code_bassin_formation": "06032", "code_postal": + "03000", "epsg_origine": "EPSG:2154", "date_maj_ligne": "2024-06-29", "coordx_origine": + 725061.3}, "geometry": {"type": "Point", "coordinates": [3.327179664489305, + 46.56509840412523]}, "record_timestamp": "2024-07-02T01:01:00Z"}, {"datasetid": + "fr-en-annuaire-education", "recordid": "9d7d1885862511baab98e460d51f4acf3c371adc", + "fields": {"libelle_zone_animation_pedagogique": "JEAN MOULIN", "hebergement": + 0, "segpa": "0", "date_ouverture": "1966-10-17", "adresse_2": "BP 133", "apprentissage": + "0", "section_theatre": "0", "lycee_agricole": "0", "libelle_bassin_formation": + "SALON DE PROVENCE", "lycee_des_metiers": "0", "section_internationale": "0", + "type_etablissement": "Coll\u00e8ge", "section_cinema": "0", "code_nature": + 340, "greta": "0", "lycee_militaire": "0", "etat": "OUVERT", "section_europeenne": + "0", "code_academie": "02", "code_commune": "13103", "voie_technologique": + "0", "post_bac": "0", "identifiant_de_l_etablissement": "0131265E", "web": + "http://www.clg-moulin-salon.ac-aix-marseille.fr", "libelle_nature": "COLLEGE", + "fiche_onisep": "https://www.onisep.fr/http/redirection/etablissement/slug/ENS.5888", + "libelle_region": "Provence-Alpes-C\u00f4te d''Azur", "section_arts": "0", + "nom_etablissement": "Coll\u00e8ge Jean Moulin", "fax": "04 90 56 38 81", + "code_bassin_formation": "02112", "code_postal": "13657", "epsg_origine": + "EPSG:2154", "date_maj_ligne": "2024-06-29", "position": [43.642784212843395, + 5.103994516073329], "statut_public_prive": "Public", "restauration": 1, "type_contrat_prive": + "SANS OBJET", "libelle_departement": "Bouches-du-Rh\u00f4ne", "voie_professionnelle": + "0", "siren_siret": "19131265100018", "code_region": "93", "voie_generale": + "0", "code_zone_animation_pedagogique": "013020", "section_sport": "1", "rpi_concentre": + 0, "ministere_tutelle": "MINISTERE DE L''EDUCATION NATIONALE", "precision_localisation": + "Num\u00e9ro de rue", "libelle_academie": "Aix-Marseille", "telephone": "04 + 90 56 14 20", "code_type_contrat_prive": "99", "longitude": 5.103994516073329, + "latitude": 43.642784212843395, "adresse_1": "Avenue de l''Europe", "coordy_origine": + 6284900.0, "code_circonscription": "0134012R", "nombre_d_eleves": 500, "appartenance_education_prioritaire": + "REP", "multi_uai": 0, "mail": "ce.0131265E@ac-aix-marseille.fr", "nom_circonscription": + "Circonscription d''inspection du 1er degr\u00e9 d''Arles - ASH Ouest", "ulis": + 0, "pial": "0133492A", "nom_commune": "Salon-de-Provence", "code_departement": + "013", "coordx_origine": 869791.0}, "geometry": {"type": "Point", "coordinates": + [5.103994516073329, 43.642784212843395]}, "record_timestamp": "2024-07-02T01:01:00Z"}, + {"datasetid": "fr-en-annuaire-education", "recordid": "4feceb0ba417c9c01ffdeabca67d467285cd3565", + "fields": {"libelle_zone_animation_pedagogique": "JEAN MOULIN", "hebergement": + 0, "date_ouverture": "1971-06-15", "libelle_bassin_formation": "SALON DE PROVENCE", + "type_etablissement": "Ecole", "code_nature": 151, "etat": "OUVERT", "code_academie": + "02", "ecole_elementaire": 1, "code_commune": "13103", "identifiant_de_l_etablissement": + "0132152U", "libelle_nature": "ECOLE DE NIVEAU ELEMENTAIRE", "libelle_region": + "Provence-Alpes-C\u00f4te d''Azur", "nom_etablissement": "Ecole \u00e9l\u00e9mentaire + Saint Norbert", "code_bassin_formation": "02112", "code_postal": "13300", + "epsg_origine": "EPSG:2154", "date_maj_ligne": "2024-06-29", "position": [43.649631956501274, + 5.104010330876341], "statut_public_prive": "Public", "restauration": 1, "type_contrat_prive": + "SANS OBJET", "ecole_maternelle": 0, "libelle_departement": "Bouches-du-Rh\u00f4ne", + "siren_siret": "21130103100269", "code_region": "93", "code_zone_animation_pedagogique": + "013020", "rpi_concentre": 0, "ministere_tutelle": "MINISTERE DE L''EDUCATION + NATIONALE", "precision_localisation": "Num\u00e9ro de rue", "libelle_academie": + "Aix-Marseille", "telephone": "0490534878", "code_type_contrat_prive": "99", + "longitude": 5.104010330876341, "latitude": 43.649631956501274, "adresse_1": + "Boulevard des Nations Unies", "coordy_origine": 6285660.8, "code_circonscription": + "0131315J", "nombre_d_eleves": 116, "appartenance_education_prioritaire": + "REP", "adresse_3": "13300 SALON DE PROVENCE", "multi_uai": 0, "mail": "ce.0132152U@ac-aix-marseille.fr", + "nom_circonscription": "Circonscription d''inspection du 1er degr\u00e9 de + Salon de Provence", "ulis": 0, "pial": "0131143X", "nom_commune": "Salon-de-Provence", + "code_departement": "013", "coordx_origine": 869772.0}, "geometry": {"type": + "Point", "coordinates": [5.104010330876341, 43.649631956501274]}, "record_timestamp": + "2024-07-02T01:01:00Z"}, {"datasetid": "fr-en-annuaire-education", "recordid": + "a31fb73e330d1cf9dfad9cbce5c581f0c9071157", "fields": {"hebergement": 0, "segpa": + "0", "date_ouverture": "1977-06-03", "adresse_2": "BP 55", "apprentissage": + "0", "section_theatre": "0", "lycee_agricole": "0", "libelle_bassin_formation": + "ALBERTVILLE", "lycee_des_metiers": "0", "section_internationale": "0", "type_etablissement": + "Coll\u00e8ge", "section_cinema": "0", "code_nature": 340, "greta": "1", "lycee_militaire": + "0", "etat": "OUVERT", "section_europeenne": "0", "code_academie": "08", "code_commune": + "73011", "voie_technologique": "0", "post_bac": "0", "identifiant_de_l_etablissement": + "0731224J", "web": "https://jean-moulin.ent.auvergnerhonealpes.fr/", "libelle_nature": + "COLLEGE", "fiche_onisep": "https://www.onisep.fr/http/redirection/etablissement/slug/ENS.16243", + "libelle_region": "Auvergne-Rh\u00f4ne-Alpes", "section_arts": "0", "nom_etablissement": + "Coll\u00e8ge Jean Moulin", "fax": "04 79 32 03 84", "code_bassin_formation": + "08734", "code_postal": "73202", "epsg_origine": "EPSG:2154", "date_maj_ligne": + "2024-06-29", "position": [45.673982216909764, 6.389100607462976], "statut_public_prive": + "Public", "restauration": 1, "type_contrat_prive": "SANS OBJET", "libelle_departement": + "Savoie", "voie_professionnelle": "0", "siren_siret": "19731224200013", "code_region": + "84", "voie_generale": "0", "section_sport": "0", "rpi_concentre": 0, "ministere_tutelle": + "MINISTERE DE L''EDUCATION NATIONALE", "precision_localisation": "Num\u00e9ro + de rue", "libelle_academie": "Grenoble", "telephone": "04 79 32 49 03", "code_type_contrat_prive": + "99", "longitude": 6.389100607462976, "latitude": 45.673982216909764, "adresse_1": + "12 rue F\u00e9lix Chautemps", "coordy_origine": 6513930.7, "code_circonscription": + "0730061V", "nombre_d_eleves": 335, "multi_uai": 0, "mail": "Ce.0731224J@ac-grenoble.fr", + "nom_circonscription": "Circonscription d''inspection du 1er degr\u00e9 de + Chamb\u00e9ry 2 - ASH", "ulis": 1, "pial": "0731224J", "nom_commune": "Albertville", + "code_departement": "073", "coordx_origine": 963765.4}, "geometry": {"type": + "Point", "coordinates": [6.389100607462976, 45.673982216909764]}, "record_timestamp": + "2024-07-02T01:01:00Z"}]}' + recorded_at: Tue, 02 Jul 2024 13:54:04 GMT recorded_with: VCR 6.2.0 diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 1ba5bfa92..664f656b1 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -10,6 +10,10 @@ describe 'The user' do log_in(user, procedure) fill_individual + + # wait for react components to be initialized + find('.dom-ready') + # fill data fill_in('text', with: 'super texte', match: :first) fill_in('textarea', with: 'super textarea') @@ -37,16 +41,23 @@ describe 'The user' do select('Martinique', from: form_id_for('regions')) select('02 – Aisne', from: form_id_for('departements')) + scroll_to(find_field('communes'), align: :center) fill_in('communes', with: '60400') find('.fr-menu__item', text: 'Brétigny (60400)').click wait_until { champ_value_for('communes') == "Brétigny" } + scroll_to(find_field('address'), align: :center) fill_in('address', with: '78 Rue du Grés 30310 Vergè') find('.fr-menu__item', text: '78 Rue du Grés 30310 Vergèze').click wait_until { champ_value_for('address') == '78 Rue du Grés 30310 Vergèze' } wait_until { champ_for('address').full_address? } expect(champ_for('address').departement_code_and_name).to eq('30 – Gard') + scroll_to(find_field('annuaire_education'), align: :center) + fill_in('annuaire_education', with: 'Moulin') + find('.fr-menu__item', text: 'Ecole primaire Jean Moulin, Moulins (0030323K)').click + wait_until { champ_for('annuaire_education').external_id == "0030323K" } + fill_in('dossier_link', with: '123') find('.editable-champ-piece_justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/file.pdf') From c6f1d1645138ffd4a4b2794eef984ec71c81dac1 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 2 Jul 2024 22:18:32 +0200 Subject: [PATCH 22/39] feat(combobox): reset value on form reset --- app/javascript/components/ComboBox.tsx | 52 +++++++++++++------ app/javascript/components/react-aria/hooks.ts | 42 +++++++++++++-- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/app/javascript/components/ComboBox.tsx b/app/javascript/components/ComboBox.tsx index 63abf1a88..3bee420c9 100644 --- a/app/javascript/components/ComboBox.tsx +++ b/app/javascript/components/ComboBox.tsx @@ -22,6 +22,7 @@ import { useMultiList, useSingleList, useRemoteList, + useOnFormReset, createLoader, type ComboBoxProps } from './react-aria/hooks'; @@ -102,7 +103,7 @@ export function SingleComboBox({ const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); - const { selectedItem, ...comboBoxProps } = useSingleList({ + const { selectedItem, onReset, ...comboBoxProps } = useSingleList({ defaultItems, defaultSelectedKey, emptyFilterKey, @@ -122,6 +123,7 @@ export function SingleComboBox({ field={formValue == 'text' ? 'label' : 'value'} name={name} form={form} + onReset={onReset} data={data} /> ) : null} @@ -150,18 +152,24 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) { const { ref, dispatch } = useDispatchChangeEvent(); const inputRef = useRef(null); - const { selectedItems, hiddenInputValues, onRemove, ...comboBoxProps } = - useMultiList({ - defaultItems, - defaultSelectedKeys, - onChange: dispatch, - formValue, - allowsCustomValue, - valueSeparator, - focusInput: () => { - inputRef.current?.focus(); - } - }); + const { + selectedItems, + hiddenInputValues, + onRemove, + onReset, + ...comboBoxProps + } = useMultiList({ + defaultItems, + defaultSelectedKeys, + onChange: dispatch, + formValue, + allowsCustomValue, + valueSeparator, + focusInput: () => { + inputRef.current?.focus(); + } + }); + const formResetRef = useOnFormReset(onReset); return (
    @@ -193,12 +201,13 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) { {name ? ( - {hiddenInputValues.map((value) => ( + {hiddenInputValues.map((value, i) => ( ))} @@ -238,7 +247,7 @@ export function RemoteComboBox({ : loader, [loader, minimumInputLength, limit] ); - const { selectedItem, ...comboBoxProps } = useRemoteList({ + const { selectedItem, onReset, ...comboBoxProps } = useRemoteList({ allowsCustomValue, defaultItems, defaultSelectedKey, @@ -270,6 +279,7 @@ export function RemoteComboBox({ } name={name} form={form} + onReset={onReset} data={data} /> ) : null} @@ -285,11 +295,13 @@ export function ComboBoxValueSlot({ field, name, form, + onReset, data }: { field: 'label' | 'value' | 'data'; name: string; form?: string; + onReset?: () => void; data?: Record; }) { const selectedItem = useContext(SelectedItemContext); @@ -300,8 +312,16 @@ export function ComboBoxValueSlot({ value ]) ); + const ref = useOnFormReset(onReset); return ( - + ); } diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts index 7974e304e..2c75e8540 100644 --- a/app/javascript/components/react-aria/hooks.ts +++ b/app/javascript/components/react-aria/hooks.ts @@ -107,6 +107,10 @@ export function useSingleList({ } } ); + const onReset = useEvent(() => { + setSelectedKey(null); + setInputValue(''); + }); // reset default selected key when props change useEffect(() => { @@ -122,7 +126,8 @@ export function useSingleList({ onSelectionChange, inputValue, onInputChange, - items: filteredItems + items: filteredItems, + onReset }; } @@ -272,6 +277,11 @@ export function useMultiList({ } ); + const onReset = useEvent(() => { + setSelectedKeys(new Set()); + setInputValue(''); + }); + return { onRemove, onSelectionChange, @@ -279,7 +289,8 @@ export function useMultiList({ selectedItems, items: filteredItems, hiddenInputValues, - inputValue + inputValue, + onReset }; } @@ -357,6 +368,11 @@ export function useRemoteList({ } ); + const onReset = useEvent(() => { + setSelectedItem(null); + setInputValue(''); + }); + // add to items list current selected item if it's not in the list const items = selectedItem && !list.getItem(selectedItem.value) @@ -369,7 +385,8 @@ export function useRemoteList({ onSelectionChange, inputValue, onInputChange, - items + items, + onReset }; } @@ -436,3 +453,22 @@ function findLabelledbyId(id?: string) { } return label.id; } + +export function useOnFormReset(onReset?: () => void) { + const ref = useRef(null); + const onResetListener = useEvent((event) => { + if (event.target == ref.current?.form) { + onReset?.(); + } + }); + useEffect(() => { + if (onReset) { + addEventListener('reset', onResetListener); + return () => { + removeEventListener('reset', onResetListener); + }; + } + }, [onReset, onResetListener]); + + return ref; +} From 14a1bfa1a38b75215a4db117f7e04ccdf5680187 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 4 Jun 2024 23:06:34 +0200 Subject: [PATCH 23/39] refactor(js): use superstruct instead of zod --- app/javascript/components/ComboBox.tsx | 7 +- app/javascript/components/react-aria/hooks.ts | 7 +- app/javascript/components/react-aria/props.ts | 129 +++++++++-------- .../controllers/lazy/tiptap_controller.ts | 39 ++++-- app/javascript/shared/tiptap/actions.ts | 21 ++- app/javascript/shared/tiptap/tags.ts | 15 +- app/javascript/shared/utils.ts | 131 ++++++++++-------- bun.lockb | Bin 555324 -> 555332 bytes package.json | 4 +- 9 files changed, 201 insertions(+), 152 deletions(-) diff --git a/app/javascript/components/ComboBox.tsx b/app/javascript/components/ComboBox.tsx index 3bee420c9..d93b957e8 100644 --- a/app/javascript/components/ComboBox.tsx +++ b/app/javascript/components/ComboBox.tsx @@ -15,6 +15,7 @@ import { import { useMemo, useRef, createContext, useContext } from 'react'; import type { RefObject } from 'react'; import { findOrCreateContainerElement } from '@coldwired/react'; +import * as s from 'superstruct'; import { useLabelledBy, @@ -98,7 +99,7 @@ export function SingleComboBox({ form, data, ...props - } = useMemo(() => SingleComboBoxProps.parse(maybeProps), [maybeProps]); + } = useMemo(() => s.create(maybeProps, SingleComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); @@ -146,7 +147,7 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) { allowsCustomValue, valueSeparator, ...props - } = useMemo(() => MultiComboBoxProps.parse(maybeProps), [maybeProps]); + } = useMemo(() => s.create(maybeProps, MultiComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); @@ -235,7 +236,7 @@ export function RemoteComboBox({ form, data, ...props - } = useMemo(() => RemoteComboBoxProps.parse(maybeProps), [maybeProps]); + } = useMemo(() => s.create(maybeProps, RemoteComboBoxProps), [maybeProps]); const labelledby = useLabelledBy(props.id, ariaLabelledby); const { ref, dispatch } = useDispatchChangeEvent(); diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts index 2c75e8540..e2683b919 100644 --- a/app/javascript/components/react-aria/hooks.ts +++ b/app/javascript/components/react-aria/hooks.ts @@ -9,6 +9,7 @@ import { matchSorter } from 'match-sorter'; import { useDebounceCallback } from 'usehooks-ts'; import { useEvent } from 'react-use-event-hook'; import isEqual from 'react-fast-compare'; +import * as s from 'superstruct'; import { Item } from './props'; @@ -420,9 +421,9 @@ export const createLoader: ( }); if (response.ok) { const json = await response.json(); - const result = Item.array().safeParse(json); - if (result.success) { - const items = matchSorter(result.data, filterText, { + const [err, result] = s.validate(json, s.array(Item), { coerce: true }); + if (!err) { + const items = matchSorter(result, filterText, { keys: ['label'] }); return { diff --git a/app/javascript/components/react-aria/props.ts b/app/javascript/components/react-aria/props.ts index e67ac1096..835b086ea 100644 --- a/app/javascript/components/react-aria/props.ts +++ b/app/javascript/components/react-aria/props.ts @@ -1,72 +1,81 @@ import type { ReactNode } from 'react'; -import { z } from 'zod'; +import * as s from 'superstruct'; import type { Loader } from './hooks'; -export const Item = z.object({ - label: z.string(), - value: z.string(), - data: z.any().optional() +export const Item = s.object({ + label: s.string(), + value: s.string(), + data: s.any() }); -export type Item = z.infer; +export type Item = s.Infer; -const ComboBoxPropsSchema = z - .object({ - id: z.string(), - className: z.string(), - name: z.string(), - label: z.string(), - description: z.string(), - isRequired: z.boolean(), - 'aria-label': z.string(), - 'aria-labelledby': z.string(), - 'aria-describedby': z.string(), - items: z - .array(Item) - .or( - z - .string() - .array() - .transform((items) => - items.map((label) => ({ label, value: label })) - ) - ) - .or( - z - .tuple([z.string(), z.string().or(z.number())]) - .array() - .transform((items) => - items.map(([label, value]) => ({ - label, - value: String(value) - })) - ) - ), - formValue: z.enum(['text', 'key']), - form: z.string(), - data: z.record(z.string()) +const ArrayOfTuples = s.coerce( + s.array(Item), + s.array(s.tuple([s.string(), s.union([s.string(), s.number()])])), + (items) => + items.map(([label, value]) => ({ + label, + value: String(value) + })) +); + +const ArrayOfStrings = s.coerce(s.array(Item), s.array(s.string()), (items) => + items.map((label) => ({ label, value: label })) +); + +const ComboBoxPropsSchema = s.partial( + s.object({ + id: s.string(), + className: s.string(), + name: s.string(), + label: s.string(), + description: s.string(), + isRequired: s.boolean(), + 'aria-label': s.string(), + 'aria-labelledby': s.string(), + 'aria-describedby': s.string(), + items: s.union([s.array(Item), ArrayOfStrings, ArrayOfTuples]), + formValue: s.enums(['text', 'key']), + form: s.string(), + data: s.record(s.string(), s.string()) }) - .partial(); -export const SingleComboBoxProps = ComboBoxPropsSchema.extend({ - selectedKey: z.string().nullable(), - emptyFilterKey: z.string() -}).partial(); -export const MultiComboBoxProps = ComboBoxPropsSchema.extend({ - selectedKeys: z.string().array(), - allowsCustomValue: z.boolean(), - valueSeparator: z.string() -}).partial(); -export const RemoteComboBoxProps = ComboBoxPropsSchema.extend({ - selectedKey: z.string().nullable(), - minimumInputLength: z.number(), - limit: z.number(), - allowsCustomValue: z.boolean() -}).partial(); -export type SingleComboBoxProps = z.infer & { +); +export const SingleComboBoxProps = s.assign( + ComboBoxPropsSchema, + s.partial( + s.object({ + selectedKey: s.nullable(s.string()), + emptyFilterKey: s.string() + }) + ) +); +export const MultiComboBoxProps = s.assign( + ComboBoxPropsSchema, + s.partial( + s.object({ + selectedKeys: s.array(s.string()), + allowsCustomValue: s.boolean(), + valueSeparator: s.string() + }) + ) +); +export const RemoteComboBoxProps = s.assign( + ComboBoxPropsSchema, + s.partial( + s.object({ + selectedKey: s.nullable(s.string()), + minimumInputLength: s.number(), + limit: s.number(), + allowsCustomValue: s.boolean() + }) + ) +); +export type SingleComboBoxProps = s.Infer & { children?: ReactNode; }; -export type MultiComboBoxProps = z.infer; -export type RemoteComboBoxProps = z.infer & { +export type MultiComboBoxProps = s.Infer; +export type RemoteComboBoxProps = s.Infer & { children?: ReactNode; loader: Loader | string; onChange?: (item: Item | null) => void; diff --git a/app/javascript/controllers/lazy/tiptap_controller.ts b/app/javascript/controllers/lazy/tiptap_controller.ts index e80d2e599..58336e926 100644 --- a/app/javascript/controllers/lazy/tiptap_controller.ts +++ b/app/javascript/controllers/lazy/tiptap_controller.ts @@ -1,6 +1,6 @@ -import { Editor, type JSONContent } from '@tiptap/core'; +import { Editor } from '@tiptap/core'; import { isButtonElement, isHTMLElement } from '@coldwired/utils'; -import { z } from 'zod'; +import * as s from 'superstruct'; import { ApplicationController } from '../application_controller'; import { getAction } from '../../shared/tiptap/actions'; @@ -61,7 +61,7 @@ export class TiptapController extends ApplicationController { insertTag(event: MouseEvent) { if (this.#editor && isHTMLElement(event.target)) { - const tag = tagSchema.parse(event.target.dataset); + const tag = s.create(event.target.dataset, tagSchema); const editor = this.#editor .chain() .focus() @@ -77,12 +77,12 @@ export class TiptapController extends ApplicationController { private get content() { const value = this.inputTarget.value; if (value) { - return jsonContentSchema.parse(JSON.parse(value)); + return s.create(JSON.parse(value), jsonContentSchema); } } private get tags(): TagSchema[] { - return this.tagTargets.map((tag) => tagSchema.parse(tag.dataset)); + return this.tagTargets.map((tag) => s.create(tag.dataset, tagSchema)); } private get menuButtons() { @@ -92,13 +92,24 @@ export class TiptapController extends ApplicationController { } } -const jsonContentSchema: z.ZodType = z.object({ - type: z.string().optional(), - text: z.string().optional(), - attrs: z.record(z.any()).optional(), - marks: z - .object({ type: z.string(), attrs: z.record(z.any()).optional() }) - .array() - .optional(), - content: z.lazy(() => z.array(jsonContentSchema).optional()) +const Attrs = s.record(s.string(), s.any()); +const Marks = s.array( + s.type({ + type: s.string(), + attrs: s.optional(Attrs) + }) +); +type JSONContent = { + type?: string; + text?: string; + attrs?: s.Infer; + marks?: s.Infer; + content?: JSONContent[]; +}; +const jsonContentSchema: s.Describe = s.type({ + type: s.optional(s.string()), + text: s.optional(s.string()), + attrs: s.optional(Attrs), + marks: s.optional(Marks), + content: s.lazy(() => s.optional(s.array(jsonContentSchema))) }); diff --git a/app/javascript/shared/tiptap/actions.ts b/app/javascript/shared/tiptap/actions.ts index 727331112..6a1d6fcae 100644 --- a/app/javascript/shared/tiptap/actions.ts +++ b/app/javascript/shared/tiptap/actions.ts @@ -1,5 +1,5 @@ import { Editor } from '@tiptap/core'; -import { z } from 'zod'; +import * as s from 'superstruct'; type EditorAction = { run(): void; @@ -11,7 +11,7 @@ export function getAction( editor: Editor, button: HTMLButtonElement ): EditorAction { - return tiptapActionSchema.parse(button.dataset)(editor); + return s.create(button.dataset, tiptapActionSchema)(editor); } const EDITOR_ACTIONS: Record EditorAction> = { @@ -109,8 +109,15 @@ const EDITOR_ACTIONS: Record EditorAction> = { }) }; -const tiptapActionSchema = z - .object({ - tiptapAction: z.enum(Object.keys(EDITOR_ACTIONS) as [string, ...string[]]) - }) - .transform(({ tiptapAction }) => EDITOR_ACTIONS[tiptapAction]); +const EditorActionFn = s.define<(editor: Editor) => EditorAction>( + 'EditorActionFn', + (fn) => typeof fn == 'function' +); + +const tiptapActionSchema = s.coerce( + EditorActionFn, + s.type({ + tiptapAction: s.enums(Object.keys(EDITOR_ACTIONS) as [string, ...string[]]) + }), + ({ tiptapAction }) => EDITOR_ACTIONS[tiptapAction] +); diff --git a/app/javascript/shared/tiptap/tags.ts b/app/javascript/shared/tiptap/tags.ts index 3a76ca1cd..fc79ffeb5 100644 --- a/app/javascript/shared/tiptap/tags.ts +++ b/app/javascript/shared/tiptap/tags.ts @@ -1,12 +1,17 @@ import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'; -import { z } from 'zod'; +import * as s from 'superstruct'; import tippy, { type Instance as TippyInstance } from 'tippy.js'; import { matchSorter } from 'match-sorter'; -export const tagSchema = z - .object({ tagLabel: z.string(), tagId: z.string() }) - .transform(({ tagId, tagLabel }) => ({ label: tagLabel, id: tagId })); -export type TagSchema = z.infer; +export const tagSchema = s.coerce( + s.object({ label: s.string(), id: s.string() }), + s.type({ + tagLabel: s.string(), + tagId: s.string() + }), + ({ tagId, tagLabel }) => ({ label: tagLabel, id: tagId }) +); +export type TagSchema = s.Infer; class SuggestionMenu { #selectedIndex = 0; diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index 5a756a850..8602ff8e8 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -1,72 +1,87 @@ import { session } from '@hotwired/turbo'; -import { z } from 'zod'; +import * as s from 'superstruct'; -const Gon = z - .object({ - autosave: z - .object({ - debounce_delay: z.number().default(0), - status_visible_duration: z.number().default(0) - }) - .default({}), - autocomplete: z - .object({ - api_geo_url: z.string().optional(), - api_adresse_url: z.string().optional(), - api_education_url: z.string().optional() - }) - .default({}), - locale: z.string().default('fr'), - matomo: z - .object({ - cookieDomain: z.string().optional(), - domain: z.string().optional(), - enabled: z.boolean().default(false), - host: z.string().optional(), - key: z.string().or(z.number()).nullish() - }) - .default({}), - sentry: z - .object({ - key: z.string().nullish(), - enabled: z.boolean().default(false), - environment: z.string().optional(), - user: z.object({ id: z.string() }).default({ id: '' }), - browser: z.object({ modern: z.boolean() }).default({ modern: false }), - release: z.string().nullish() - }) - .default({}), - crisp: z - .object({ - key: z.string().nullish(), - enabled: z.boolean().default(false), - administrateur: z - .object({ - email: z.string(), - DS_SIGN_IN_COUNT: z.number(), - DS_NB_DEMARCHES_BROUILLONS: z.number(), - DS_NB_DEMARCHES_ACTIVES: z.number(), - DS_NB_DEMARCHES_ARCHIVES: z.number(), - DS_ID: z.number() - }) - .default({ +function nullish(struct: s.Struct) { + return s.optional(s.union([s.literal(null), struct])); +} + +const Gon = s.defaulted( + s.type({ + autosave: s.defaulted( + s.type({ + debounce_delay: s.defaulted(s.number(), 0), + status_visible_duration: s.defaulted(s.number(), 0) + }), + {} + ), + autocomplete: s.defaulted( + s.partial( + s.type({ + api_geo_url: s.string(), + api_adresse_url: s.string(), + api_education_url: s.string() + }) + ), + {} + ), + locale: s.defaulted(s.string(), 'fr'), + matomo: s.defaulted( + s.type({ + cookieDomain: s.optional(s.string()), + domain: s.optional(s.string()), + enabled: s.defaulted(s.boolean(), false), + host: s.optional(s.string()), + key: nullish(s.union([s.string(), s.number()])) + }), + {} + ), + sentry: s.defaulted( + s.type({ + key: nullish(s.string()), + enabled: s.defaulted(s.boolean(), false), + environment: s.optional(s.string()), + user: s.defaulted(s.type({ id: s.string() }), { id: '' }), + browser: s.defaulted(s.type({ modern: s.boolean() }), { + modern: false + }), + release: nullish(s.string()) + }), + {} + ), + crisp: s.defaulted( + s.type({ + key: nullish(s.string()), + enabled: s.defaulted(s.boolean(), false), + administrateur: s.defaulted( + s.type({ + email: s.string(), + DS_SIGN_IN_COUNT: s.number(), + DS_NB_DEMARCHES_BROUILLONS: s.number(), + DS_NB_DEMARCHES_ACTIVES: s.number(), + DS_NB_DEMARCHES_ARCHIVES: s.number(), + DS_ID: s.number() + }), + { email: '', DS_SIGN_IN_COUNT: 0, DS_NB_DEMARCHES_BROUILLONS: 0, DS_NB_DEMARCHES_ACTIVES: 0, DS_NB_DEMARCHES_ARCHIVES: 0, DS_ID: 0 - }) - }) - .default({}), - defaultQuery: z.string().optional(), - defaultVariables: z.string().optional() - }) - .default({}); + } + ) + }), + {} + ), + defaultQuery: s.optional(s.string()), + defaultVariables: s.optional(s.string()) + }), + {} +); declare const window: Window & typeof globalThis & { gon: unknown }; export function getConfig() { - return Gon.parse(window.gon); + return s.create(window.gon, Gon); } export function show(el: Element | null) { diff --git a/bun.lockb b/bun.lockb index 608dba50f0f296e4f8b60dd549f48a635868a277..beab4b76049ce3f26b347be09fc37335f1eb6dcf 100755 GIT binary patch delta 58730 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%0A2|MYrba#s_Xq?ln%dAj~mb{&dV_3*lxn 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 delta 58645 zcma&P2Vhl2^Z$KMk`wNs_Ygu10TBU1hZ_jwiXcUrh=8FQAV44_kkADZRKSLUM_dD< zPec$K3Oii3wl*XT;q-2M_4;)aOqv>))ZtoV>9C z`3#K>1S$jqMfq8oW2S+dmJ0+bf)_y@=z(RACl&TVs4`$$R(?TNK4ogEgJ(g@LnlK; zi4EodqRcUQ`MD_jJ{Snp1|L8u9y$P_1ZYQS9Q0-QLI=`gE$Ef>Bb|TG57B!`wAYW@ z-%7UXxl^sU_!?Dp57ny$0#%Th0Tsy!Ziy9F7^GJ0m`IiAiK?OL)B+i5AaKy&b~W{Q zE{9e^?l4r^O~}-OZySCRRK|0$+?4#h$=L;2fy{!;DWdlT&Oo%VD4z~Q(SuMi>lCA> zShR>A($P9bEBs=pm_NCWHe{5c*F&ZKl~5EGWfx@T=V#6iv<1t6#$*;`j=JNpH$=*%ui8K09kI?%Gd?)*!`-wb9tiar9%AV%kBj>#$r1QsW2 zxjj&6pX4r!j%(%jvA=nG>?d zOrUduZB`caG59jQvs&nWresc=kTNN2_NmLnz=46Hf~?6iWQF=8pPiYLlU*<^aIpvc zmD<0hR`j)}AW-y2D{VLqltBkl+y!=AuPv=LcRf^Quzed%ld(m@G7c)keXy{oe6V1qRdUPh##Rm2iB0N|oT}U8wAb`mu;>~FmMH4qK~rCDLSEh^8CiEX zGd3>1H&WuTzdGs+GiE|&&Lr$z;Vz4f%TA@10556*m3f|#IxZ)3d_f?ovzc$G^p%@8 zWtxmKuOKUT2DC-GmTv@=@#JKWn>Kw)AdsI`ke4$fD=;=32(TE6hC-#jk6W!=oD(pp zs@tkuQsGJ3$`oZ6n^<(NhUpKF~E#bO!C>{b|{`vr}^N1I>DCflCan3Kava zK6*CBLnYby$uGlQ7EG%4ZC~w|{LGmtqw~hj9_4Ni#yJi9Ywm|`Sun1ZAJtx#!?Z6@ zE1IU~XQ#;0n4CF_&|}U9XjjyBn>b0?U9Zzp2aNQuV3~7G(R2+GK&EC($Fj*;aRh6X#?N)_o6k_d9Xb`h#RLJ{+RCd)(igB&UYqmUolO zH;&&pNXz)L1=BL8O)tnQ*yCoF4|S8fOxw$NiKuv;6d=YfFS7=d9F$kGE|4ebK zYgLTvFdeRpYCPFw!ql9=w1S*$)CDHOm$sQuvDXjY$yxd1vswoN)7=3T;|kl7J0%i- zfy#RClC8C6VV-1y?S~R8rXHLkR_UI%% z;s*FtdLNyamou8JxgA0>24m$lfsYYj*DI<|MJ=JZx?_JB6)$WM_=$xi2c_w`*|W08 z%?|imZlT+%Qj2Q)NJY|JW2l(5&7E4QajUoUHFsuaer|T|_`n3PxH&FX^Zj}YTpRup zcUPsP!X7hqPmfL4E@%PfkWsX8hIUy!s7 P#My7v$X5R^qB^(ejvfb# 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 Z)EXf_zC(c{@>>dwP_OH# Date: Mon, 8 Jul 2024 10:27:56 +0200 Subject: [PATCH 24/39] fix(combobox): allow non unique lists of options --- app/javascript/components/react-aria/hooks.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts index e2683b919..200ba16d7 100644 --- a/app/javascript/components/react-aria/hooks.ts +++ b/app/javascript/components/react-aria/hooks.ts @@ -56,7 +56,10 @@ export function useSingleList({ onChange?: (item: Item | null) => void; }) { const [selectedKey, setSelectedKey] = useState(defaultSelectedKey); - const items = useMemo(() => defaultItems || [], [defaultItems]); + const items = useMemo( + () => (defaultItems ? distinctBy(defaultItems, 'value') : []), + [defaultItems] + ); const selectedItem = useMemo( () => items.find((item) => item.value == selectedKey) ?? null, [items, selectedKey] @@ -157,7 +160,10 @@ export function useMultiList({ () => new Set(defaultSelectedKeys ?? []) ); const [inputValue, setInputValue] = useState(''); - const items = useMemo(() => defaultItems || [], [defaultItems]); + const items = useMemo( + () => (defaultItems ? distinctBy(defaultItems, 'value') : []), + [defaultItems] + ); const itemsIndex = useMemo(() => { const index = new Map(); for (const item of items) { @@ -473,3 +479,8 @@ export function useOnFormReset(onReset?: () => void) { return ref; } + +function distinctBy(array: T[], key: keyof T): T[] { + const keys = array.map((item) => item[key]); + return array.filter((item, index) => keys.indexOf(item[key]) == index); +} From 029db3b1cb0ec5865ae2639f7a0e0c2c1488d2b6 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 15:36:19 +0200 Subject: [PATCH 25/39] refactor(alert): standard methods order --- app/components/dsfr/alert_component.rb | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/app/components/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb index 7323f639b..dd18f4a14 100644 --- a/app/components/dsfr/alert_component.rb +++ b/app/components/dsfr/alert_component.rb @@ -2,6 +2,17 @@ class Dsfr::AlertComponent < ApplicationComponent renders_one :body + attr_reader :state, :title, :size, :block, :extra_class_names, :heading_level + + def initialize(state:, title: '', size: '', extra_class_names: nil, heading_level: 'h3') + @state = state + @title = title + @size = size + @block = block + @extra_class_names = extra_class_names + @heading_level = heading_level + end + def prefix_for_state case state when :error then "Erreur : " @@ -19,19 +30,4 @@ class Dsfr::AlertComponent < ApplicationComponent extra_class_names => true ) end - - private - - def initialize(state:, title: '', size: '', extra_class_names: nil, heading_level: 'h3') - @state = state - @title = title - @size = size - @block = block - @extra_class_names = extra_class_names - @heading_level = heading_level - end - - attr_reader :state, :title, :size, :block, :extra_class_names, :heading_level - - private end From 28e8c786fa92398f088eb976f88c74e0a01d3369 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 14:09:08 +0200 Subject: [PATCH 26/39] chore: don't wrap "en test" tag/breadcrumb --- config/locales/views/layouts/_breadcrumb.fr.yml | 2 +- .../procedures/show.html.haml_spec.rb | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/config/locales/views/layouts/_breadcrumb.fr.yml b/config/locales/views/layouts/_breadcrumb.fr.yml index 9605fb9ef..2cb0ed409 100644 --- a/config/locales/views/layouts/_breadcrumb.fr.yml +++ b/config/locales/views/layouts/_breadcrumb.fr.yml @@ -10,7 +10,7 @@ fr: since: "depuis le %{date}" closed: "Close" published: "Publiée" - draft: "En test" + draft: "En test" more_info_on_test: "Pour plus d’information sur la phase de test" go_to_FAQ: "consulter la FAQ" url_FAQ: "/faq#accordion-administrateur-2" diff --git a/spec/views/administrateurs/procedures/show.html.haml_spec.rb b/spec/views/administrateurs/procedures/show.html.haml_spec.rb index 57224b13b..da9cbe1e6 100644 --- a/spec/views/administrateurs/procedures/show.html.haml_spec.rb +++ b/spec/views/administrateurs/procedures/show.html.haml_spec.rb @@ -16,17 +16,11 @@ describe 'administrateurs/procedures/show', type: :view do render end - describe 'publish button is visible' do - it { expect(rendered).to have_css('#publish-procedure-link') } - it { expect(rendered).not_to have_css('#close-procedure-link') } - end - - describe 'procedure path is not customized' do - it { expect(rendered).to have_content('En test') } - end - - describe 'archive button' do - it { expect(rendered).not_to have_css('#archive-procedure') } + it "render content" do + expect(rendered).to have_css('#publish-procedure-link') + expect(rendered).not_to have_css('#close-procedure-link') + expect(rendered).to have_content('En test') + expect(rendered).not_to have_css('#archive-procedure') end end end From ba6fdc47488b52d64eb3d5ac5f31991d3f66314f Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 3 Jul 2024 15:37:24 +0200 Subject: [PATCH 27/39] feat: /commencer alert about draft procedure --- .../procedure_draft_warning_component.rb | 21 ++++++++++ .../procedure_draft_warning_component.en.yml | 13 ++++++ .../procedure_draft_warning_component.fr.yml | 13 ++++++ ...rocedure_draft_warning_component.html.haml | 10 +++++ app/views/commencer/show.html.haml | 4 ++ spec/views/commencer/show.html.haml_spec.rb | 40 +++++++++++++++++-- 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 app/components/procedure_draft_warning_component.rb create mode 100644 app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml create mode 100644 app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml create mode 100644 app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml diff --git a/app/components/procedure_draft_warning_component.rb b/app/components/procedure_draft_warning_component.rb new file mode 100644 index 000000000..10c409600 --- /dev/null +++ b/app/components/procedure_draft_warning_component.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ProcedureDraftWarningComponent < ApplicationComponent + attr_reader :revision + attr_reader :current_administrateur + attr_reader :extra_class_names + + def initialize(revision:, current_administrateur:, extra_class_names: nil) + @revision = revision + @current_administrateur = current_administrateur + @extra_class_names = extra_class_names + end + + def render? + revision.draft? + end + + def admin? + current_administrateur.present? && revision.procedure.administrateurs.include?(current_administrateur) + end +end diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml new file mode 100644 index 000000000..6f8dc61bb --- /dev/null +++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml @@ -0,0 +1,13 @@ +--- +en: + title: Procedure in testing + intro_procedure_brouillon_html: This procedure is currently in testing + intro_revision_draft_html: This page allows you to test a new version of the procedure + body_general_html: | + and this page is reserved for the administration in charge of its deployment. + If you start or submit a file, it may be deleted at any time without notice, even if it is accepted later. + body_user: | + If this link was shared with you, please contact the service in charge of this procedure + to obtain the public link for the procedure in order to submit your application. + body_admin_procedure_brouillon: Do not share this link with your users. When you publish the procedure, you will access the public link for the procedure to be shared. + body_admin_revision_draft: Do not share this link with your users, but rather the public link for the procedure displayed in your administrator dashboard. diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml new file mode 100644 index 000000000..5b0bc7f0f --- /dev/null +++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml @@ -0,0 +1,13 @@ +--- +fr: + title: Démarche en test + intro_procedure_brouillon_html: Cette démarche est actuellement en test + intro_revision_draft_html: Cette page permet de tester une nouvelle version de la démarche + body_general_html: | + et cette page est réservée à l’administration en charge de son déploiement. + Si vous commencez ou déposez un dossier, il pourra être supprimé à tout moment et sans préavis, même après avoir été accepté. + body_user: | + Si ce lien vous a été communiqué, contactez le service en charge de cette démarche + pour obtenir le lien public de la démarche afin de déposer votre dossier. + body_admin_procedure_brouillon: Ne communiquez pas ce lien à vos usagers. Lorsque vous publierez la démarche, vous accéderez au lien public de la démarche à communiquer. + body_admin_revision_draft: Ne communiquez pas ce lien à vos usagers, mais le lien public de la démarche affiché dans votre tableau de bord administrateur. diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml new file mode 100644 index 000000000..929b1e378 --- /dev/null +++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml @@ -0,0 +1,10 @@ += render Dsfr::AlertComponent.new(state: :warning, extra_class_names:, title: t(".title")) do |c| + - c.with_body do + %p + = revision.procedure.brouillon? ? t(".intro_procedure_brouillon_html") : t(".intro_revision_draft_html") + = t(".body_general_html") + + - if admin? + %p= revision.procedure.brouillon? ? t(".body_admin_procedure_brouillon") : t(".body_admin_revision_draft") + - else + %p= t(".body_user") diff --git a/app/views/commencer/show.html.haml b/app/views/commencer/show.html.haml index 55b7a35c9..545789f98 100644 --- a/app/views/commencer/show.html.haml +++ b/app/views/commencer/show.html.haml @@ -13,7 +13,11 @@ #{Current.application_name} %li= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--secondary' + = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w") + - else + = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w") + - if @prefilled_dossier = render Dsfr::CalloutComponent.new(title: t(".prefilled_draft"), heading_level: 'h2') do |c| - c.with_body do diff --git a/spec/views/commencer/show.html.haml_spec.rb b/spec/views/commencer/show.html.haml_spec.rb index 571654477..bcda8b893 100644 --- a/spec/views/commencer/show.html.haml_spec.rb +++ b/spec/views/commencer/show.html.haml_spec.rb @@ -7,10 +7,15 @@ RSpec.describe 'commencer/show', type: :view do let(:drafts) { [] } let(:not_drafts) { [] } let(:preview_dossiers) { dossiers.take(3) } + let(:user) { nil } + + before do + allow(view).to receive(:current_administrateur).and_return(user&.administrateur) + end before do assign(:procedure, procedure) - assign(:revision, procedure.published_revision) + assign(:revision, procedure.active_revision) assign(:dossiers, dossiers) assign(:drafts, drafts) assign(:not_drafts, not_drafts) @@ -25,8 +30,6 @@ RSpec.describe 'commencer/show', type: :view do subject { render } context 'when no user is signed in' do - let(:user) { nil } - it 'renders sign-in and sign-up links' do subject expect(rendered).to have_link('Créer un compte') @@ -98,4 +101,35 @@ RSpec.describe 'commencer/show', type: :view do end end end + + context "procedure is draft" do + let(:procedure) { create(:procedure, :draft) } + let(:user) { create :user } + + it 'renders a warning' do + subject + expect(rendered).to have_text("Cette démarche est actuellement en test") + end + + context "when user is admin" do + let(:user) { procedure.administrateurs.first.user } + + it "renders warning about draft" do + subject + expect(rendered).to have_text("Cette démarche est actuellement en test") + expect(rendered).to have_text("Ne communiquez pas ce lien") + end + end + end + + context "revision is draft" do + before { + assign(:revision, procedure.draft_revision) + } + + it "renders warning about draft" do + subject + expect(rendered).to have_text("Démarche en test") + end + end end From 133689b4d37b293bc28bebb60bb810a2d5bde8a2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 8 Jul 2024 11:42:40 +0200 Subject: [PATCH 28/39] refactor(champ): remove call to validation from value formatting code --- app/models/champ.rb | 5 ----- app/models/types_de_champ/decimal_number_type_de_champ.rb | 2 +- app/models/types_de_champ/integer_number_type_de_champ.rb | 2 +- app/models/types_de_champ/type_de_champ_base.rb | 4 ++-- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/models/champ.rb b/app/models/champ.rb index c144fb470..0f3eb9525 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -107,11 +107,6 @@ class Champ < ApplicationRecord [to_s] end - def valid_value - return unless valid_champ_value? - value - end - def to_s TypeDeChamp.champ_value(type_champ, self) end diff --git a/app/models/types_de_champ/decimal_number_type_de_champ.rb b/app/models/types_de_champ/decimal_number_type_de_champ.rb index 9455283f5..486c4e5c8 100644 --- a/app/models/types_de_champ/decimal_number_type_de_champ.rb +++ b/app/models/types_de_champ/decimal_number_type_de_champ.rb @@ -20,7 +20,7 @@ class TypesDeChamp::DecimalNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase private def champ_formatted_value(champ) - champ.valid_value&.to_f + champ.value&.to_f end end end diff --git a/app/models/types_de_champ/integer_number_type_de_champ.rb b/app/models/types_de_champ/integer_number_type_de_champ.rb index 7c2d3ef58..515f475aa 100644 --- a/app/models/types_de_champ/integer_number_type_de_champ.rb +++ b/app/models/types_de_champ/integer_number_type_de_champ.rb @@ -20,7 +20,7 @@ class TypesDeChamp::IntegerNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase private def champ_formatted_value(champ) - champ.valid_value&.to_i + champ.value&.to_i end end end diff --git a/app/models/types_de_champ/type_de_champ_base.rb b/app/models/types_de_champ/type_de_champ_base.rb index 81217caa2..02570c8d1 100644 --- a/app/models/types_de_champ/type_de_champ_base.rb +++ b/app/models/types_de_champ/type_de_champ_base.rb @@ -66,12 +66,12 @@ class TypesDeChamp::TypeDeChampBase when 2 champ_value(champ) else - champ.valid_value.presence || champ_default_api_value(version) + champ.value.presence || champ_default_api_value(version) end end def champ_value_for_export(champ, path = :value) - path == :value ? champ.valid_value.presence : champ_default_export_value(path) + path == :value ? champ.value.presence : champ_default_export_value(path) end def champ_value_for_tag(champ, path = :value) From 96e49d54cd97a1dd723da57cbc80e54c90645305 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 8 Jul 2024 15:44:30 +0200 Subject: [PATCH 29/39] reafctor(logic): remove tests and accept unspecified behaviour --- spec/models/logic/champ_value_spec.rb | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/spec/models/logic/champ_value_spec.rb b/spec/models/logic/champ_value_spec.rb index f7b12020f..c287c57d4 100644 --- a/spec/models/logic/champ_value_spec.rb +++ b/spec/models/logic/champ_value_spec.rb @@ -40,12 +40,6 @@ describe Logic::ChampValue do it { is_expected.to be nil } end - - context 'with invalid value' do - before { champ.value = 'environ 300' } - - it { is_expected.to be nil } - end end context 'decimal tdc' do @@ -53,18 +47,6 @@ describe Logic::ChampValue do it { expect(champ_value(champ.stable_id).type([champ.type_de_champ])).to eq(:number) } it { is_expected.to eq(42.01) } - - context 'with invalid value with too many digits after the decimal point' do - before { champ.value = '42.1234' } - - it { is_expected.to be nil } - end - - context 'with invalid value' do - before { champ.value = 'racine de 2' } - - it { is_expected.to be nil } - end end context 'dropdown tdc' do From a46ebea4eecaff057b6adafe4012fe96d6cf7019 Mon Sep 17 00:00:00 2001 From: Benoit Queyron Date: Tue, 11 Jun 2024 14:44:25 +0200 Subject: [PATCH 30/39] =?UTF-8?q?d=C3=A9tachement=20de=20l'attribut=20for?= =?UTF-8?q?=20dans=20le=20label=20d'une=20PJ=20avec=20l'id=20de=20l'input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../champ_label_component.html.haml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml index 0be774df0..76e67c44c 100644 --- a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml +++ b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml @@ -1,8 +1,13 @@ = # we do this trick because some html elements should use 'label' and some should be plain paragraphs - if @champ.html_label? - = @form.label @champ.main_value_name, id: @champ.labelledby_id, for: @champ.input_id, class: 'fr-label' do - - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at + - if @champ.piece_justificative? + -# champ piece_justificative : remove the asociation with the input + = @form.label @champ.main_value_name, id: @champ.labelledby_id, class: 'fr-label' do + - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at + - else + = @form.label @champ.main_value_name, id: @champ.labelledby_id, for: @champ.input_id, class: 'fr-label' do + - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at - elsif @champ.legend_label? %legend.fr-fieldset__legend.fr-text--regular{ id: @champ.labelledby_id }= render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at - elsif @champ.single_checkbox? From bfd455986f9dcec3f9bdbb9bbc3a430163e69c72 Mon Sep 17 00:00:00 2001 From: Benoit Queyron Date: Tue, 11 Jun 2024 17:15:02 +0200 Subject: [PATCH 31/39] ajout de la condition sur PJ uniquement autorisee --- .../champ_label_component/champ_label_component.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml index 76e67c44c..fd896be6a 100644 --- a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml +++ b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml @@ -1,7 +1,7 @@ = # we do this trick because some html elements should use 'label' and some should be plain paragraphs - if @champ.html_label? - - if @champ.piece_justificative? + - if @champ.piece_justificative? && !@champ.procedure.piece_justificative_multiple -# champ piece_justificative : remove the asociation with the input = @form.label @champ.main_value_name, id: @champ.labelledby_id, class: 'fr-label' do - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at From a2389f14af970831a52ba7cb867f52f2463fde7b Mon Sep 17 00:00:00 2001 From: Benoit Queyron Date: Fri, 14 Jun 2024 17:58:21 +0200 Subject: [PATCH 32/39] =?UTF-8?q?annulation=20des=20modifs=20pr=C3=A9c?= =?UTF-8?q?=C3=A9dentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../champ_label_component.html.haml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml index fd896be6a..0be774df0 100644 --- a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml +++ b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml @@ -1,13 +1,8 @@ = # we do this trick because some html elements should use 'label' and some should be plain paragraphs - if @champ.html_label? - - if @champ.piece_justificative? && !@champ.procedure.piece_justificative_multiple - -# champ piece_justificative : remove the asociation with the input - = @form.label @champ.main_value_name, id: @champ.labelledby_id, class: 'fr-label' do - - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at - - else - = @form.label @champ.main_value_name, id: @champ.labelledby_id, for: @champ.input_id, class: 'fr-label' do - - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at + = @form.label @champ.main_value_name, id: @champ.labelledby_id, for: @champ.input_id, class: 'fr-label' do + - render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at - elsif @champ.legend_label? %legend.fr-fieldset__legend.fr-text--regular{ id: @champ.labelledby_id }= render EditableChamp::ChampLabelContentComponent.new form: @form, champ: @champ, seen_at: @seen_at - elsif @champ.single_checkbox? From 70c1d30fe4e47725096b153b26b86fe82dcb7cc8 Mon Sep 17 00:00:00 2001 From: Benoit Queyron <72251526+Benoit-MINT@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:05:48 +0200 Subject: [PATCH 33/39] disabled attribut on input --- app/components/attachment/edit_component.rb | 8 ++++++-- .../multiple_component/multiple_component.html.haml | 4 ++-- app/controllers/attachments_controller.rb | 1 + app/views/attachments/destroy.turbo_stream.haml | 6 ++++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index e096591ef..d78aeba91 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -9,8 +9,9 @@ class Attachment::EditComponent < ApplicationComponent alias as_multiple? as_multiple EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze + DEFAULT_MAX_ATTACHMENTS = 10 - def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], **kwargs) + def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], max: nil, **kwargs) @champ = champ @attached_file = attached_file @direct_upload = direct_upload @@ -24,6 +25,7 @@ class Attachment::EditComponent < ApplicationComponent @attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : []) @attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty? @attachments.compact! + @max = max || DEFAULT_MAX_ATTACHMENTS # Utilisation du premier attachement comme référence pour la rétrocompatibilité @attachment = @attachments.first @@ -54,7 +56,7 @@ class Attachment::EditComponent < ApplicationComponent end def destroy_attachment_path - attachment_path(champ_id: champ&.public_id) + attachment_path(champ_id: champ&.public_id, champ: @champ) end def attachment_input_class @@ -63,6 +65,7 @@ class Attachment::EditComponent < ApplicationComponent def file_field_options track_issue_with_missing_validators if missing_validators? + options = { class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?), direct_upload: @direct_upload, @@ -76,6 +79,7 @@ class Attachment::EditComponent < ApplicationComponent options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {}) options[:multiple] = true if as_multiple? + options[:disabled] = true if @index >= @max options end diff --git a/app/components/attachment/multiple_component/multiple_component.html.haml b/app/components/attachment/multiple_component/multiple_component.html.haml index 74abdf23c..40b94577f 100644 --- a/app/components/attachment/multiple_component/multiple_component.html.haml +++ b/app/components/attachment/multiple_component/multiple_component.html.haml @@ -7,8 +7,8 @@ %li{ id: dom_id(attachment) } = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, view_as:, user_can_destroy:, form_object_name:) - %div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?), data: { turbo_force: :server } } - = render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) + %div{ id: empty_component_id, data: { turbo_force: :server } } + = render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:, max: @max) // single poll and refresh message for all attachments = render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context) diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 410c64212..449f3050c 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -22,6 +22,7 @@ class AttachmentsController < ApplicationController flash.notice = 'La pièce jointe a bien été supprimée.' @champ_id = params[:champ_id] + @champ = Champ.find(params[:champ]) if params[:champ] respond_to do |format| format.turbo_stream diff --git a/app/views/attachments/destroy.turbo_stream.haml b/app/views/attachments/destroy.turbo_stream.haml index 48cee0bc3..beebaa232 100644 --- a/app/views/attachments/destroy.turbo_stream.haml +++ b/app/views/attachments/destroy.turbo_stream.haml @@ -5,3 +5,9 @@ = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input" = turbo_stream.show_all ".attachment-input-#{@attachment.id}" + +- if @champ + = fields_for @champ.input_name, @champ do |form| + = turbo_stream.replace @champ.input_group_id do + = render EditableChamp::EditableChampComponent.new champ: @champ, form: form + = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input" From 4bee40caa4d959eb96e437bcbd8d734380c92029 Mon Sep 17 00:00:00 2001 From: Benoit Queyron <72251526+Benoit-MINT@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:19:51 +0200 Subject: [PATCH 34/39] suppresion de la logique hidden --- app/components/attachment/edit_component.rb | 7 +++---- app/components/attachment/multiple_component.rb | 4 ---- app/controllers/attachments_controller.rb | 1 - app/views/attachments/destroy.turbo_stream.haml | 6 +----- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index d78aeba91..a0dc0d3d6 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -9,7 +9,6 @@ class Attachment::EditComponent < ApplicationComponent alias as_multiple? as_multiple EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze - DEFAULT_MAX_ATTACHMENTS = 10 def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], max: nil, **kwargs) @champ = champ @@ -25,7 +24,7 @@ class Attachment::EditComponent < ApplicationComponent @attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : []) @attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty? @attachments.compact! - @max = max || DEFAULT_MAX_ATTACHMENTS + @max = max # Utilisation du premier attachement comme référence pour la rétrocompatibilité @attachment = @attachments.first @@ -56,7 +55,7 @@ class Attachment::EditComponent < ApplicationComponent end def destroy_attachment_path - attachment_path(champ_id: champ&.public_id, champ: @champ) + attachment_path(champ: @champ) end def attachment_input_class @@ -79,7 +78,7 @@ class Attachment::EditComponent < ApplicationComponent options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {}) options[:multiple] = true if as_multiple? - options[:disabled] = true if @index >= @max + options[:disabled] = true if @max && @index >= @max options end diff --git a/app/components/attachment/multiple_component.rb b/app/components/attachment/multiple_component.rb index 80d2fd941..f994ed1cd 100644 --- a/app/components/attachment/multiple_component.rb +++ b/app/components/attachment/multiple_component.rb @@ -30,10 +30,6 @@ class Attachment::MultipleComponent < ApplicationComponent @attachments.each_with_index(&block) end - def can_attach_next? - @attachments.count < @max - end - def empty_component_id champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic" end diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 449f3050c..4e53e804f 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -21,7 +21,6 @@ class AttachmentsController < ApplicationController @attachment.purge_later flash.notice = 'La pièce jointe a bien été supprimée.' - @champ_id = params[:champ_id] @champ = Champ.find(params[:champ]) if params[:champ] respond_to do |format| diff --git a/app/views/attachments/destroy.turbo_stream.haml b/app/views/attachments/destroy.turbo_stream.haml index beebaa232..e66582528 100644 --- a/app/views/attachments/destroy.turbo_stream.haml +++ b/app/views/attachments/destroy.turbo_stream.haml @@ -1,13 +1,9 @@ = turbo_stream.remove dom_id(@attachment, :persisted_row) -- if @champ_id - = turbo_stream.show "attachment-multiple-empty-#{@champ_id}" - = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input" - = turbo_stream.show_all ".attachment-input-#{@attachment.id}" - if @champ = fields_for @champ.input_name, @champ do |form| = turbo_stream.replace @champ.input_group_id do = render EditableChamp::EditableChampComponent.new champ: @champ, form: form - = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input" + = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ.public_id} input" From fc385208dbb7d2316d2f841173264e664898f9fe Mon Sep 17 00:00:00 2001 From: Benoit Queyron <72251526+Benoit-MINT@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:44:22 +0200 Subject: [PATCH 35/39] update test --- spec/components/attachment/multiple_component_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/components/attachment/multiple_component_spec.rb b/spec/components/attachment/multiple_component_spec.rb index 26dfacd2c..143831280 100644 --- a/spec/components/attachment/multiple_component_spec.rb +++ b/spec/components/attachment/multiple_component_spec.rb @@ -75,8 +75,8 @@ RSpec.describe Attachment::MultipleComponent, type: :component do context 'max attachments' do let(:kwargs) { { max: 1 } } - it 'does not render visible input file where max attachments has been reached' do - expect(subject).to have_selector('.hidden input[type=file]') + it 'renders a disabled input file where max attachments has been reached' do + expect(subject).to have_selector('input[type=file][disabled]') end end From ebea9e381887e50e16afa4216750c7457894e957 Mon Sep 17 00:00:00 2001 From: Benoit Queyron <72251526+Benoit-MINT@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:27:39 +0200 Subject: [PATCH 36/39] find champ by dossier stable_id row_id --- app/components/attachment/edit_component.rb | 2 +- app/controllers/attachments_controller.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index a0dc0d3d6..117ea2a8c 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -55,7 +55,7 @@ class Attachment::EditComponent < ApplicationComponent end def destroy_attachment_path - attachment_path(champ: @champ) + attachment_path(dossier_id: champ&.dossier_id, stable_id: champ&.stable_id, row_id: champ&.row_id) end def attachment_input_class diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 4e53e804f..721ec2cc2 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -21,11 +21,18 @@ class AttachmentsController < ApplicationController @attachment.purge_later flash.notice = 'La pièce jointe a bien été supprimée.' - @champ = Champ.find(params[:champ]) if params[:champ] + @champ = find_champ if params[:dossier_id] respond_to do |format| format.turbo_stream format.html { redirect_back(fallback_location: root_url) } end end + + private + + def find_champ + dossier = policy_scope(Dossier).includes(:champs).find(params[:dossier_id]) + dossier.champs.find_by(stable_id: params[:stable_id], row_id: params[:row_id]) + end end From 142ed6f762c2facfbedc3ccbc7947102d2cb3107 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Tue, 9 Jul 2024 12:48:53 +0200 Subject: [PATCH 37/39] fix(cojo): fetch external data when "-01" suffix is typed, even if its deleted later by .to_i --- app/models/champs/cojo_champ.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/champs/cojo_champ.rb b/app/models/champs/cojo_champ.rb index 514db2a8e..3a356e838 100644 --- a/app/models/champs/cojo_champ.rb +++ b/app/models/champs/cojo_champ.rb @@ -50,7 +50,7 @@ class Champs::COJOChamp < Champ def update_external_id if accreditation_number_changed? || accreditation_birthdate_changed? - if accreditation_number.present? && accreditation_birthdate.present? && /\A\d+\z/.match?(accreditation_number) + if accreditation_number.present? && accreditation_birthdate.present? && /\A[\d-]+\z/.match?(accreditation_number) self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json else self.external_id = nil From 94684711646c78225447f5588858a53eca50002c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 9 Jul 2024 16:16:07 +0200 Subject: [PATCH 38/39] fix(combobox): allow for null emptyFilterKey and fix setSelection --- app/javascript/components/react-aria/hooks.ts | 6 +++--- app/javascript/components/react-aria/props.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts index 200ba16d7..e00bff289 100644 --- a/app/javascript/components/react-aria/hooks.ts +++ b/app/javascript/components/react-aria/hooks.ts @@ -52,7 +52,7 @@ export function useSingleList({ }: { defaultItems?: Item[]; defaultSelectedKey?: string | null; - emptyFilterKey?: string; + emptyFilterKey?: string | null; onChange?: (item: Item | null) => void; }) { const [selectedKey, setSelectedKey] = useState(defaultSelectedKey); @@ -85,8 +85,8 @@ export function useSingleList({ const initialSelectedKeyRef = useRef(defaultSelectedKey); const setSelection = useEvent((key?: string | null) => { - const inputValue = defaultSelectedKey - ? items.find((item) => item.value == defaultSelectedKey)?.label + const inputValue = key + ? items.find((item) => item.value == key)?.label : ''; setSelectedKey(key); setInputValue(inputValue ?? ''); diff --git a/app/javascript/components/react-aria/props.ts b/app/javascript/components/react-aria/props.ts index 835b086ea..4932551c5 100644 --- a/app/javascript/components/react-aria/props.ts +++ b/app/javascript/components/react-aria/props.ts @@ -46,7 +46,7 @@ export const SingleComboBoxProps = s.assign( s.partial( s.object({ selectedKey: s.nullable(s.string()), - emptyFilterKey: s.string() + emptyFilterKey: s.nullable(s.string()) }) ) ); From d1cbf766abf130ac15caafd7d7f0f44a0c895b39 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 10 Jul 2024 10:18:04 +0200 Subject: [PATCH 39/39] fix(react): disable arai components locales optimization --- vite.config.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index f0f0dfde8..48ab90a9c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vite'; import ViteReact from '@vitejs/plugin-react'; import RubyPlugin from 'vite-plugin-ruby'; import FullReload from 'vite-plugin-full-reload'; -import optimizeLocales from '@react-aria/optimize-locales-plugin'; +//import optimizeLocales from '@react-aria/optimize-locales-plugin'; const plugins = [ RubyPlugin(), @@ -10,13 +10,13 @@ const plugins = [ FullReload( ['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'], { delay: 200 } - ), - { - ...optimizeLocales.vite({ - locales: ['en-GB', 'fr-FR'] - }), - enforce: 'pre' as const - } + ) + // { + // ...optimizeLocales.vite({ + // locales: ['en-GB', 'fr-FR'] + // }), + // enforce: 'pre' as const + // } ]; export default defineConfig({