feat(champ): ask for departement before asking for commune
This commit is contained in:
parent
dc56572b1d
commit
ba0211ba52
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 { 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 d’Armor', '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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue