From 1e11ad4ce6b124ed77e49a81151d7c2324c43713 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 6 May 2024 18:07:29 +0200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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; +}