feat(champ): ask for departement before asking for commune

This commit is contained in:
Paul Chavard 2021-10-26 16:21:47 +02:00 committed by Paul Chavard
parent dc56572b1d
commit ba0211ba52
7 changed files with 132 additions and 49 deletions

View file

@ -1,9 +1,10 @@
import React from 'react'; import React, { useState, useMemo } from 'react';
import { QueryClientProvider } from 'react-query'; import { QueryClientProvider } from 'react-query';
import { matchSorter } from 'match-sorter'; import { matchSorter } from 'match-sorter';
import ComboSearch from './ComboSearch'; import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient'; import { queryClient } from './shared/queryClient';
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
// Avoid hiding similar matches for precise queries (like "Sainte Marie") // Avoid hiding similar matches for precise queries (like "Sainte Marie")
function searchResultsLimit(term) { function searchResultsLimit(term) {
@ -36,20 +37,71 @@ function expandResultsWithMultiplePostalCodes(term, results) {
return expandedResults; return expandedResults;
} }
const placeholderDepartements = [
['63 Puy-de-Dôme', 'Clermont-Ferrand'],
['77 Seine-et-Marne', 'Melun'],
['22 Côtes dArmor', 'Saint-Brieuc'],
['47 Lot-et-Garonne', 'Agen']
];
const [placeholderDepartement, placeholderCommune] =
placeholderDepartements[
Math.floor(Math.random() * (placeholderDepartements.length - 1))
];
function ComboCommunesSearch(params) { function ComboCommunesSearch(params) {
const [departementCode, setDepartementCode] = useState();
const inputId = useMemo(
() =>
document.querySelector(`input[data-uuid="${params.hiddenFieldId}"]`)?.id,
[params.hiddenFieldId]
);
const departementDescribedBy = `${inputId}_departement_notice`;
const communeDescribedBy = `${inputId}_commune_notice`;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ComboSearch <div>
required={params.mandatory} <div className="notice" id={departementDescribedBy}>
hiddenFieldId={params.hiddenFieldId} <p>
scope="communes" Choisissez le département dans lequel se situe la commune. Vous
minimumInputLength={2} pouvez entrer le nom ou le code.
transformResult={({ code, nom, codesPostaux }) => [ </p>
code, </div>
`${nom} (${codesPostaux[0]})` <ComboDepartementsSearch
]} inputId={!departementCode ? inputId : null}
transformResults={expandResultsWithMultiplePostalCodes} aria-describedby={departementDescribedBy}
/> placeholder={placeholderDepartement}
mandatory={params.mandatory}
onChange={(_, result) => {
setDepartementCode(result?.code);
}}
/>
</div>
{departementCode ? (
<div>
<div className="notice" id={communeDescribedBy}>
<p>
Choisissez la commune. Vous pouver entre le nom ou le code postal.
</p>
</div>
<ComboSearch
autoFocus
inputId={inputId}
aria-describedby={communeDescribedBy}
placeholder={placeholderCommune}
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
scope="communes"
scopeExtra={departementCode}
minimumInputLength={2}
transformResult={({ code, nom, codesPostaux }) => [
code,
`${nom} (${codesPostaux[0]})`
]}
transformResults={expandResultsWithMultiplePostalCodes}
/>
</div>
) : null}
</QueryClientProvider> </QueryClientProvider>
); );
} }

View file

@ -16,19 +16,27 @@ function expandResultsWithForeignDepartement(term, results) {
]; ];
} }
function ComboDepartementsSearch(params) { export function ComboDepartementsSearch(params) {
return (
<ComboSearch
{...params}
scope="departements"
minimumInputLength={1}
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
transformResults={expandResultsWithForeignDepartement}
/>
);
}
function ComboDepartementsSearchDefault(params) {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ComboSearch <ComboDepartementsSearch
required={params.mandatory} required={params.mandatory}
hiddenFieldId={params.hiddenFieldId} hiddenFieldId={params.hiddenFieldId}
scope="departements"
minimumInputLength={1}
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
transformResults={expandResultsWithForeignDepartement}
/> />
</QueryClientProvider> </QueryClientProvider>
); );
} }
export default ComboDepartementsSearch; export default ComboDepartementsSearchDefault;

View file

@ -1,4 +1,10 @@
import React, { useState, useMemo, useCallback, useRef } from 'react'; import React, {
useState,
useMemo,
useCallback,
useRef,
useEffect
} from 'react';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -19,22 +25,25 @@ function defaultTransformResults(_, results) {
} }
function ComboSearch({ function ComboSearch({
placeholder,
required,
hiddenFieldId, hiddenFieldId,
onChange, onChange,
scope, scope,
inputId,
scopeExtra,
minimumInputLength, minimumInputLength,
transformResult, transformResult,
allowInputValues = false, allowInputValues = false,
transformResults = defaultTransformResults, transformResults = defaultTransformResults,
className ...props
}) { }) {
const label = scope;
const hiddenValueField = useMemo( const hiddenValueField = useMemo(
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`), () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
[hiddenFieldId] [hiddenFieldId]
); );
const comboInputId = useMemo(
() => hiddenValueField?.id || inputId,
[inputId, hiddenValueField]
);
const hiddenIdField = useMemo( const hiddenIdField = useMemo(
() => () =>
document.querySelector( document.querySelector(
@ -81,15 +90,18 @@ function ComboSearch({
const handleOnChange = useCallback( const handleOnChange = useCallback(
({ target: { value } }) => { ({ target: { value } }) => {
setValue(value); setValue(value);
if (value.length >= minimumInputLength) { if (!value) {
setExternalId('');
setExternalValue('');
if (onChange) {
onChange(null);
}
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim()); setSearchTerm(value.trim());
if (allowInputValues) { if (allowInputValues) {
setExternalId(''); setExternalId('');
setExternalValue(value); setExternalValue(value);
} }
} else if (!value) {
setExternalId('');
setExternalValue('');
} }
}, },
[minimumInputLength] [minimumInputLength]
@ -102,11 +114,14 @@ function ComboSearch({
awaitFormSubmit.done(); awaitFormSubmit.done();
}, []); }, []);
const { isSuccess, data } = useQuery([scope, debouncedSearchTerm], { const { isSuccess, data } = useQuery(
enabled: !!debouncedSearchTerm, [scope, debouncedSearchTerm, scopeExtra],
notifyOnStatusChange: false, {
refetchOnMount: false enabled: !!debouncedSearchTerm,
}); notifyOnStatusChange: false,
refetchOnMount: false
}
);
const results = isSuccess ? transformResults(debouncedSearchTerm, data) : []; const results = isSuccess ? transformResults(debouncedSearchTerm, data) : [];
const onBlur = useCallback(() => { const onBlur = useCallback(() => {
@ -118,15 +133,20 @@ function ComboSearch({
} }
}, [data]); }, [data]);
useEffect(() => {
document
.querySelector(`#${comboInputId}[type="hidden"]`)
?.removeAttribute('id');
}, [comboInputId]);
return ( return (
<Combobox aria-label={label} onSelect={handleOnSelect}> <Combobox onSelect={handleOnSelect}>
<ComboboxInput <ComboboxInput
className={className} {...props}
placeholder={placeholder} id={comboInputId}
onChange={handleOnChange} onChange={handleOnChange}
onBlur={onBlur} onBlur={onBlur}
value={value} value={value}
required={required}
/> />
{isSuccess && ( {isSuccess && (
<ComboboxPopover className="shadow-popup"> <ComboboxPopover className="shadow-popup">
@ -157,8 +177,6 @@ function ComboSearch({
} }
ComboSearch.propTypes = { ComboSearch.propTypes = {
placeholder: PropTypes.string,
required: PropTypes.bool,
hiddenFieldId: PropTypes.string, hiddenFieldId: PropTypes.string,
scope: PropTypes.string, scope: PropTypes.string,
minimumInputLength: PropTypes.number, minimumInputLength: PropTypes.number,
@ -166,7 +184,8 @@ ComboSearch.propTypes = {
transformResults: PropTypes.func, transformResults: PropTypes.func,
allowInputValues: PropTypes.bool, allowInputValues: PropTypes.bool,
onChange: PropTypes.func, onChange: PropTypes.func,
className: PropTypes.string inputId: PropTypes.string,
scopeExtra: PropTypes.string
}; };
export default ComboSearch; export default ComboSearch;

View file

@ -26,17 +26,21 @@ export const queryClient = new QueryClient({
} }
}); });
function buildURL(scope, term) { function buildURL(scope, term, extra) {
term = encodeURIComponent(term.replace(/\(|\)/g, '')); term = encodeURIComponent(term.replace(/\(|\)/g, ''));
if (scope === 'adresse') { if (scope === 'adresse') {
return `${api_adresse_url}/search?q=${term}&limit=${API_ADRESSE_QUERY_LIMIT}`; return `${api_adresse_url}/search?q=${term}&limit=${API_ADRESSE_QUERY_LIMIT}`;
} else if (scope === 'annuaire-education') { } else if (scope === 'annuaire-education') {
return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=${API_EDUCATION_QUERY_LIMIT}`; return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=${API_EDUCATION_QUERY_LIMIT}`;
} else if (scope === 'communes') { } else if (scope === 'communes') {
const limit = `limit=${API_GEO_COMMUNES_QUERY_LIMIT}`;
const url = extra
? `${api_geo_url}/communes?codeDepartement=${extra}&${limit}&`
: `${api_geo_url}/communes?${limit}&`;
if (isNumeric(term)) { if (isNumeric(term)) {
return `${api_geo_url}/communes?codePostal=${term}&limit=${API_GEO_COMMUNES_QUERY_LIMIT}`; return `${url}codePostal=${term}`;
} }
return `${api_geo_url}/communes?nom=${term}&boost=population&limit=${API_GEO_COMMUNES_QUERY_LIMIT}`; return `${url}nom=${term}&boost=population`;
} else if (isNumeric(term)) { } else if (isNumeric(term)) {
const code = term.padStart(2, '0'); const code = term.padStart(2, '0');
return `${api_geo_url}/${scope}?code=${code}&limit=${API_GEO_QUERY_LIMIT}`; return `${api_geo_url}/${scope}?code=${code}&limit=${API_GEO_QUERY_LIMIT}`;
@ -53,12 +57,12 @@ function buildOptions() {
return [{}, null]; return [{}, null];
} }
async function defaultQueryFn({ queryKey: [scope, term] }) { async function defaultQueryFn({ queryKey: [scope, term, extra] }) {
if (scope == 'pays') { if (scope == 'pays') {
return matchSorter(await getPays(), term, { keys: ['label'] }); return matchSorter(await getPays(), term, { keys: ['label'] });
} }
const url = buildURL(scope, term); const url = buildURL(scope, term, extra);
const [options, controller] = buildOptions(); const [options, controller] = buildOptions();
const promise = fetch(url, options).then((response) => { const promise = fetch(url, options).then((response) => {
if (response.ok) { if (response.ok) {

View file

@ -467,6 +467,7 @@ Rails.application.routes.draw do
get 'regions' => 'api_geo_test#regions' get 'regions' => 'api_geo_test#regions'
get 'communes' => 'api_geo_test#communes' get 'communes' => 'api_geo_test#communes'
get 'departements' => 'api_geo_test#departements' get 'departements' => 'api_geo_test#departements'
get 'departements/:code/communes' => 'api_geo_test#communes'
end end
end end

View file

@ -104,13 +104,11 @@ module SystemHelpers
end end
def select_combobox(champ, fill_with, value) def select_combobox(champ, fill_with, value)
input = find("input[aria-label=\"#{champ}\"") fill_in champ, with: fill_with
input.click
input.fill_in with: fill_with
selector = "li[data-option-value=\"#{value}\"]" selector = "li[data-option-value=\"#{value}\"]"
find(selector).click find(selector).click
expect(page).to have_css(selector) expect(page).to have_css(selector)
expect(page).to have_hidden_field(champ, with: value) expect(page).to have_css("[type=\"hidden\"][value=\"#{value}\"]")
end end
def select_multi_combobox(champ, fill_with, value) def select_multi_combobox(champ, fill_with, value)

View file

@ -32,6 +32,7 @@ describe 'The user' do
select_combobox('pays', 'aust', 'Australie') select_combobox('pays', 'aust', 'Australie')
select_combobox('regions', 'Ma', 'Martinique') select_combobox('regions', 'Ma', 'Martinique')
select_combobox('departements', 'Ai', '02 - Aisne') select_combobox('departements', 'Ai', '02 - Aisne')
select_combobox('communes', 'Ai', '02 - Aisne')
select_combobox('communes', 'Ambl', 'Ambléon (01300)') select_combobox('communes', 'Ambl', 'Ambléon (01300)')
check('engagement') check('engagement')