Merge pull request #6588 from tchak/feat-commune-by-departement
feat(champ): ask for departement before asking for commune
This commit is contained in:
commit
88df8a888e
7 changed files with 132 additions and 49 deletions
|
@ -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,20 +37,71 @@ function expandResultsWithMultiplePostalCodes(term, results) {
|
|||
return expandedResults;
|
||||
}
|
||||
|
||||
const placeholderDepartements = [
|
||||
['63 – Puy-de-Dôme', 'Clermont-Ferrand'],
|
||||
['77 – Seine-et-Marne', 'Melun'],
|
||||
['22 – Côtes d’Armor', '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}>
|
||||
<ComboSearch
|
||||
required={params.mandatory}
|
||||
hiddenFieldId={params.hiddenFieldId}
|
||||
scope="communes"
|
||||
minimumInputLength={2}
|
||||
transformResult={({ code, nom, codesPostaux }) => [
|
||||
code,
|
||||
`${nom} (${codesPostaux[0]})`
|
||||
]}
|
||||
transformResults={expandResultsWithMultiplePostalCodes}
|
||||
/>
|
||||
<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,
|
||||
`${nom} (${codesPostaux[0]})`
|
||||
]}
|
||||
transformResults={expandResultsWithMultiplePostalCodes}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ComboSearch
|
||||
<ComboDepartementsSearch
|
||||
required={params.mandatory}
|
||||
hiddenFieldId={params.hiddenFieldId}
|
||||
scope="departements"
|
||||
minimumInputLength={1}
|
||||
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
|
||||
transformResults={expandResultsWithForeignDepartement}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ComboDepartementsSearch;
|
||||
export default ComboDepartementsSearchDefault;
|
||||
|
|
|
@ -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], {
|
||||
enabled: !!debouncedSearchTerm,
|
||||
notifyOnStatusChange: false,
|
||||
refetchOnMount: false
|
||||
});
|
||||
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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue