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 { matchSorter } from 'match-sorter';
import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
function searchResultsLimit(term) {
@ -36,13 +37,62 @@ function expandResultsWithMultiplePostalCodes(term, results) {
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) {
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 (
<QueryClientProvider client={queryClient}>
<div>
<div className="notice" id={departementDescribedBy}>
<p>
Choisissez le département dans lequel se situe la commune. Vous
pouvez entrer le nom ou le code.
</p>
</div>
<ComboDepartementsSearch
inputId={!departementCode ? inputId : null}
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,
@ -50,6 +100,8 @@ function ComboCommunesSearch(params) {
]}
transformResults={expandResultsWithMultiplePostalCodes}
/>
</div>
) : null}
</QueryClientProvider>
);
}

View file

@ -16,19 +16,27 @@ function expandResultsWithForeignDepartement(term, results) {
];
}
function ComboDepartementsSearch(params) {
export function ComboDepartementsSearch(params) {
return (
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
{...params}
scope="departements"
minimumInputLength={1}
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
transformResults={expandResultsWithForeignDepartement}
/>
);
}
function ComboDepartementsSearchDefault(params) {
return (
<QueryClientProvider client={queryClient}>
<ComboDepartementsSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
/>
</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 { useQuery } from 'react-query';
import PropTypes from 'prop-types';
@ -19,22 +25,25 @@ function defaultTransformResults(_, results) {
}
function ComboSearch({
placeholder,
required,
hiddenFieldId,
onChange,
scope,
inputId,
scopeExtra,
minimumInputLength,
transformResult,
allowInputValues = false,
transformResults = defaultTransformResults,
className
...props
}) {
const label = scope;
const hiddenValueField = useMemo(
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
[hiddenFieldId]
);
const comboInputId = useMemo(
() => hiddenValueField?.id || inputId,
[inputId, hiddenValueField]
);
const hiddenIdField = useMemo(
() =>
document.querySelector(
@ -81,15 +90,18 @@ function ComboSearch({
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);
if (value.length >= minimumInputLength) {
if (!value) {
setExternalId('');
setExternalValue('');
if (onChange) {
onChange(null);
}
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
} else if (!value) {
setExternalId('');
setExternalValue('');
}
},
[minimumInputLength]
@ -102,11 +114,14 @@ function ComboSearch({
awaitFormSubmit.done();
}, []);
const { isSuccess, data } = useQuery([scope, debouncedSearchTerm], {
const { isSuccess, data } = useQuery(
[scope, debouncedSearchTerm, scopeExtra],
{
enabled: !!debouncedSearchTerm,
notifyOnStatusChange: false,
refetchOnMount: false
});
}
);
const results = isSuccess ? transformResults(debouncedSearchTerm, data) : [];
const onBlur = useCallback(() => {
@ -118,15 +133,20 @@ function ComboSearch({
}
}, [data]);
useEffect(() => {
document
.querySelector(`#${comboInputId}[type="hidden"]`)
?.removeAttribute('id');
}, [comboInputId]);
return (
<Combobox aria-label={label} onSelect={handleOnSelect}>
<Combobox onSelect={handleOnSelect}>
<ComboboxInput
className={className}
placeholder={placeholder}
{...props}
id={comboInputId}
onChange={handleOnChange}
onBlur={onBlur}
value={value}
required={required}
/>
{isSuccess && (
<ComboboxPopover className="shadow-popup">
@ -157,8 +177,6 @@ function ComboSearch({
}
ComboSearch.propTypes = {
placeholder: PropTypes.string,
required: PropTypes.bool,
hiddenFieldId: PropTypes.string,
scope: PropTypes.string,
minimumInputLength: PropTypes.number,
@ -166,7 +184,8 @@ ComboSearch.propTypes = {
transformResults: PropTypes.func,
allowInputValues: PropTypes.bool,
onChange: PropTypes.func,
className: PropTypes.string
inputId: PropTypes.string,
scopeExtra: PropTypes.string
};
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, ''));
if (scope === 'adresse') {
return `${api_adresse_url}/search?q=${term}&limit=${API_ADRESSE_QUERY_LIMIT}`;
} else if (scope === 'annuaire-education') {
return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=${API_EDUCATION_QUERY_LIMIT}`;
} 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)) {
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)) {
const code = term.padStart(2, '0');
return `${api_geo_url}/${scope}?code=${code}&limit=${API_GEO_QUERY_LIMIT}`;
@ -53,12 +57,12 @@ function buildOptions() {
return [{}, null];
}
async function defaultQueryFn({ queryKey: [scope, term] }) {
async function defaultQueryFn({ queryKey: [scope, term, extra] }) {
if (scope == 'pays') {
return matchSorter(await getPays(), term, { keys: ['label'] });
}
const url = buildURL(scope, term);
const url = buildURL(scope, term, extra);
const [options, controller] = buildOptions();
const promise = fetch(url, options).then((response) => {
if (response.ok) {

View file

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

View file

@ -104,13 +104,11 @@ module SystemHelpers
end
def select_combobox(champ, fill_with, value)
input = find("input[aria-label=\"#{champ}\"")
input.click
input.fill_in with: fill_with
fill_in champ, with: fill_with
selector = "li[data-option-value=\"#{value}\"]"
find(selector).click
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
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('regions', 'Ma', 'Martinique')
select_combobox('departements', 'Ai', '02 - Aisne')
select_combobox('communes', 'Ai', '02 - Aisne')
select_combobox('communes', 'Ambl', 'Ambléon (01300)')
check('engagement')