From 20f6e1d63c3814f1eeb256c4d9dced13e9a63f46 Mon Sep 17 00:00:00 2001 From: Enguerran Weiss Date: Mon, 3 Feb 2025 16:11:40 +0100 Subject: [PATCH] added isDebouncing in geosearch and naf slice, adjusted ui for RSAutocomplete --- .../autocomplete/AppellationAutocomplete.tsx | 2 +- .../forms/autocomplete/NafAutocomplete.tsx | 4 +- .../forms/autocomplete/PlaceAutocomplete.tsx | 4 +- front/src/app/pages/search/SearchPage.tsx | 26 ++++++++---- .../domain/geosearch/geosearch.selectors.ts | 20 ++++++++-- .../domain/geosearch/geosearch.slice.ts | 9 +++-- .../domain/geosearch/geosearch.test.ts | 20 ++++++++-- .../core-logic/domain/naf/naf.selectors.ts | 3 ++ front/src/core-logic/domain/naf/naf.slice.ts | 4 ++ front/src/core-logic/domain/naf/naf.test.ts | 10 +++++ .../rs-autocomplete/RSAutocomplete.scss | 8 +++- .../rs-autocomplete/RSAutocomplete.tsx | 40 +++++++++++-------- shared/src/naf/naf.dto.ts | 3 +- 13 files changed, 111 insertions(+), 42 deletions(-) diff --git a/front/src/app/components/forms/autocomplete/AppellationAutocomplete.tsx b/front/src/app/components/forms/autocomplete/AppellationAutocomplete.tsx index a785ecb4df..94a38fda68 100644 --- a/front/src/app/components/forms/autocomplete/AppellationAutocomplete.tsx +++ b/front/src/app/components/forms/autocomplete/AppellationAutocomplete.tsx @@ -88,7 +88,7 @@ export const AppellationAutocomplete = ({ }) => { if (!searchTerm) return "Saisissez un métier"; if (searchTerm.length < ROME_AND_APPELLATION_MIN_SEARCH_TEXT_LENGTH) - return "Saisissez au moins 3 caractères"; + return "Saisissez au moins 2 caractères"; if (isSearching || searchTerm !== debounceSearchTerm) return "..."; return "Aucun métier trouvé"; }; diff --git a/front/src/app/components/forms/autocomplete/NafAutocomplete.tsx b/front/src/app/components/forms/autocomplete/NafAutocomplete.tsx index d648d0e11d..6e6102ff00 100644 --- a/front/src/app/components/forms/autocomplete/NafAutocomplete.tsx +++ b/front/src/app/components/forms/autocomplete/NafAutocomplete.tsx @@ -22,18 +22,20 @@ export const NafAutocomplete = ({ const dispatch = useDispatch(); const [searchTerm, setSearchTerm] = useState(""); const isLoading = useAppSelector(nafSelectors.isLoading); + const isDebouncing = useAppSelector(nafSelectors.isDebouncing); const options = useAppSelector(nafSelectors.currentNafSections); return ( <>Saisissez au moins 3 caractères, placeholder: "Ex : Administration publique", onChange: (nafSectionSuggestion, actionMeta) => { if (nafSectionSuggestion && actionMeta.action === "select-option") { onNafSelected(nafSectionSuggestion.value); + dispatch(nafSlice.actions.queryWasEmptied()); } if ( actionMeta.action === "clear" || diff --git a/front/src/app/components/forms/autocomplete/PlaceAutocomplete.tsx b/front/src/app/components/forms/autocomplete/PlaceAutocomplete.tsx index 8a6bd7a542..0666f83d63 100644 --- a/front/src/app/components/forms/autocomplete/PlaceAutocomplete.tsx +++ b/front/src/app/components/forms/autocomplete/PlaceAutocomplete.tsx @@ -23,6 +23,7 @@ export const PlaceAutocomplete = ({ const [searchTerm, setSearchTerm] = useState(""); const isSearching: boolean = useAppSelector(geosearchSelectors.isLoading); + const isDebouncing: boolean = useAppSelector(geosearchSelectors.isDebouncing); const searchSuggestions = useAppSelector(geosearchSelectors.suggestions); const options = searchSuggestions.map((suggestion) => ({ value: suggestion, @@ -32,11 +33,11 @@ export const PlaceAutocomplete = ({ <>Recherche de ville en cours... 🔎, inputValue: searchTerm, - noOptionsMessage: () => <>Saisissez au moins 3 caractères, placeholder: "Ex : Saint-Denis, La Réunion, France", onChange: (searchResult, actionMeta) => { if (actionMeta.action === "clear") { @@ -50,6 +51,7 @@ export const PlaceAutocomplete = ({ searchResult.value, ), ); + dispatch(geosearchSlice.actions.queryWasEmptied()); } }, options, diff --git a/front/src/app/pages/search/SearchPage.tsx b/front/src/app/pages/search/SearchPage.tsx index 53b46957ab..57b34c688e 100644 --- a/front/src/app/pages/search/SearchPage.tsx +++ b/front/src/app/pages/search/SearchPage.tsx @@ -53,6 +53,7 @@ const radiusOptions = ["1", "2", "5", "10", "20", "50", "100"].map( value: distance, }), ); +const nafLabelMaxLength = 40; const getSearchRouteParam = ( currentKey: keyof SearchPageParams, @@ -194,7 +195,21 @@ export const SearchPage = ({ const placeInputLabel = <>...dans la ville; const shouldShowInitialScreen = searchStatus === "noSearchMade"; - + const displayAppellationsOrNaf = () => { + const appellationDisplayed = formValues.appellations?.length + ? formValues.appellations.map( + (appellation) => appellation.appellationLabel, + ) + : []; + const nafDisplayed = formValues.nafLabel + ? [ + formValues.nafLabel.length > nafLabelMaxLength + ? `${formValues.nafLabel.substring(0, nafLabelMaxLength)}...` + : formValues.nafLabel, + ] + : []; + return [...appellationDisplayed, ...nafDisplayed].join(" - "); + }; return ( @@ -337,13 +352,7 @@ export const SearchPage = ({ defaultValue="Tous les métiers" iconId="fr-icon-briefcase-fill" id={domElementIds.search.appellationFilterTag} - values={ - formValues.appellations - ? formValues.appellations.map( - (appellation) => appellation.appellationLabel, - ) - : [] - } + values={[displayAppellationsOrNaf()]} onReset={() => { const updatedValues = { ...tempValue, @@ -400,6 +409,7 @@ export const SearchPage = ({ setTempValue({ ...tempValue, nafCodes: nafSectionSuggestion.nafCodes, + nafLabel: nafSectionSuggestion.label, }); }} onNafClear={() => { diff --git a/front/src/core-logic/domain/geosearch/geosearch.selectors.ts b/front/src/core-logic/domain/geosearch/geosearch.selectors.ts index 0b87e11278..a87266212a 100644 --- a/front/src/core-logic/domain/geosearch/geosearch.selectors.ts +++ b/front/src/core-logic/domain/geosearch/geosearch.selectors.ts @@ -1,16 +1,28 @@ +import { createSelector } from "@reduxjs/toolkit"; import { createRootSelector } from "src/core-logic/storeConfig/store"; -const suggestions = createRootSelector((state) => state.geosearch.suggestions); +const geosearchState = createRootSelector((state) => state.geosearch); -const isLoading = createRootSelector((state) => state.geosearch.isLoading); +const isDebouncing = createSelector( + geosearchState, + (state) => state.isDebouncing, +); -const query = createRootSelector((state) => state.geosearch.query); +const suggestions = createSelector( + geosearchState, + (state) => state.suggestions, +); -const value = createRootSelector((state) => state.geosearch.value); +const isLoading = createSelector(geosearchState, (state) => state.isLoading); + +const query = createSelector(geosearchState, (state) => state.query); + +const value = createSelector(geosearchState, (state) => state.value); export const geosearchSelectors = { suggestions, isLoading, query, value, + isDebouncing, }; diff --git a/front/src/core-logic/domain/geosearch/geosearch.slice.ts b/front/src/core-logic/domain/geosearch/geosearch.slice.ts index fb424a4e95..f12dab88d8 100644 --- a/front/src/core-logic/domain/geosearch/geosearch.slice.ts +++ b/front/src/core-logic/domain/geosearch/geosearch.slice.ts @@ -6,6 +6,7 @@ type GeoSearchState = { value: LookupSearchResult | null; query: string; isLoading: boolean; + isDebouncing: boolean; }; const initialState: GeoSearchState = { @@ -13,18 +14,17 @@ const initialState: GeoSearchState = { query: "", value: null, isLoading: false, + isDebouncing: false, }; export const geosearchSlice = createSlice({ name: "geosearch", initialState, reducers: { - queryWasEmptied: (state) => { - state.suggestions = []; - state.value = null; - }, + queryWasEmptied: (_state) => initialState, queryHasChanged: (state, _action: PayloadAction) => { state.suggestions = []; + state.isDebouncing = true; }, suggestionsHaveBeenRequested: ( state, @@ -32,6 +32,7 @@ export const geosearchSlice = createSlice({ ) => { state.query = action.payload; state.isLoading = true; + state.isDebouncing = false; }, suggestionsSuccessfullyFetched: ( state, diff --git a/front/src/core-logic/domain/geosearch/geosearch.test.ts b/front/src/core-logic/domain/geosearch/geosearch.test.ts index b3d1a04ab4..2ecf3a5929 100644 --- a/front/src/core-logic/domain/geosearch/geosearch.test.ts +++ b/front/src/core-logic/domain/geosearch/geosearch.test.ts @@ -4,6 +4,7 @@ import { expectObjectsToMatch, expectToEqual, } from "shared"; +import { geosearchSelectors } from "src/core-logic/domain/geosearch/geosearch.selectors"; import { TestDependencies, createTestStore, @@ -37,7 +38,9 @@ describe("Geosearch epic", () => { it("should update the searched query and reset the state", () => { const query = "foi"; store.dispatch(geosearchSlice.actions.queryHasChanged(query)); + expectDebouncingToBe(true); dependencies.scheduler.flush(); + expectDebouncingToBe(false); expectLoadingToBe(true); expectQueryToBe(query); }); @@ -45,6 +48,7 @@ describe("Geosearch epic", () => { it("shouldn't update the searched query if threshold is not reached", () => { const query = "fo"; store.dispatch(geosearchSlice.actions.queryHasChanged(query)); + expectDebouncingToBe(true); dependencies.scheduler.flush(); expectLoadingToBe(false); expectQueryToBe(""); @@ -62,7 +66,9 @@ describe("Geosearch epic", () => { }, ]; store.dispatch(geosearchSlice.actions.queryHasChanged(query)); + expectDebouncingToBe(true); dependencies.scheduler.flush(); + expectDebouncingToBe(false); expectLoadingToBe(true); dependencies.addressGateway.lookupLocationResults$.next( expectedSuggestions, @@ -95,15 +101,21 @@ describe("Geosearch epic", () => { }); const expectQueryToBe = (expected: string) => { - expectToEqual(store.getState().geosearch.query, expected); + expectToEqual(geosearchSelectors.query(store.getState()), expected); }; const expectLoadingToBe = (expected: boolean) => { - expectToEqual(store.getState().geosearch.isLoading, expected); + expectToEqual(geosearchSelectors.isLoading(store.getState()), expected); + }; + const expectDebouncingToBe = (expected: boolean) => { + expectToEqual(geosearchSelectors.isDebouncing(store.getState()), expected); }; const expectSuggestionsToBe = (expected: LookupSearchResult[]) => { - expectArraysToEqual(store.getState().geosearch.suggestions, expected); + expectArraysToEqual( + geosearchSelectors.suggestions(store.getState()), + expected, + ); }; const expectSelectedSuggestionToBe = (expected: LookupSearchResult) => { - expectObjectsToMatch(store.getState().geosearch.value, expected); + expectObjectsToMatch(geosearchSelectors.value(store.getState()), expected); }; }); diff --git a/front/src/core-logic/domain/naf/naf.selectors.ts b/front/src/core-logic/domain/naf/naf.selectors.ts index 1d785877df..f8d357c44b 100644 --- a/front/src/core-logic/domain/naf/naf.selectors.ts +++ b/front/src/core-logic/domain/naf/naf.selectors.ts @@ -10,7 +10,10 @@ const currentNafSections = createSelector( (state) => state.currentNafSections, ); +const isDebouncing = createSelector(nafState, (state) => state.isDebouncing); + export const nafSelectors = { isLoading, currentNafSections, + isDebouncing, }; diff --git a/front/src/core-logic/domain/naf/naf.slice.ts b/front/src/core-logic/domain/naf/naf.slice.ts index 7667524828..0b1da20dcc 100644 --- a/front/src/core-logic/domain/naf/naf.slice.ts +++ b/front/src/core-logic/domain/naf/naf.slice.ts @@ -3,11 +3,13 @@ import { NafSectionSuggestion } from "shared"; export type NafState = { isLoading: boolean; + isDebouncing: boolean; currentNafSections: NafSectionSuggestion[]; }; export const initialState: NafState = { isLoading: false, + isDebouncing: false, currentNafSections: [], }; @@ -17,6 +19,7 @@ export const nafSlice = createSlice({ reducers: { queryHasChanged: (state, _action: PayloadAction) => { state.currentNafSections = []; + state.isDebouncing = true; }, queryWasEmptied: (state) => { state.isLoading = false; @@ -24,6 +27,7 @@ export const nafSlice = createSlice({ }, searchSectionsRequested: (state, _action: PayloadAction) => { state.isLoading = true; + state.isDebouncing = false; }, searchSectionsSucceeded: ( state, diff --git a/front/src/core-logic/domain/naf/naf.test.ts b/front/src/core-logic/domain/naf/naf.test.ts index 8a45d8f605..0e4ff324b7 100644 --- a/front/src/core-logic/domain/naf/naf.test.ts +++ b/front/src/core-logic/domain/naf/naf.test.ts @@ -29,16 +29,23 @@ describe("naf slice", () => { it("should fetch naf sections and update the state", () => { expectToMatchState(store.getState(), initialState); store.dispatch(nafSlice.actions.queryHasChanged("query")); + expectToMatchState(store.getState(), { + currentNafSections: [], + isLoading: false, + isDebouncing: true, + }); dependencies.scheduler.flush(); expectToMatchState(store.getState(), { currentNafSections: [], isLoading: true, + isDebouncing: false, }); // feed gateway dependencies.nafGateway.nafSuggestions$.next(expectedResults); expectToMatchState(store.getState(), { currentNafSections: expectedResults, isLoading: false, + isDebouncing: false, }); }); @@ -49,6 +56,7 @@ describe("naf slice", () => { expectToMatchState(store.getState(), { currentNafSections: [], isLoading: true, + isDebouncing: false, }); dependencies.nafGateway.nafSuggestions$.error(new Error("test")); expectToMatchState(store.getState(), initialState); @@ -62,6 +70,7 @@ describe("naf slice", () => { expectToMatchState(store.getState(), { currentNafSections: expectedResults, isLoading: false, + isDebouncing: false, }); store.dispatch(nafSlice.actions.queryHasChanged("qu")); dependencies.scheduler.flush(); @@ -76,6 +85,7 @@ describe("naf slice", () => { expectToMatchState(store.getState(), { currentNafSections: expectedResults, isLoading: false, + isDebouncing: false, }); store.dispatch(nafSlice.actions.queryWasEmptied()); dependencies.scheduler.flush(); diff --git a/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.scss b/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.scss index 763e789616..d263d1b9ab 100644 --- a/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.scss +++ b/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.scss @@ -22,9 +22,15 @@ } &__option { &:hover { - background-color: var(--hover-tint); + background-color: var(--background-overlap-grey-hover); cursor: pointer; } + &--is-focused { + background-color: var(--background-overlap-grey-hover); + } + } + &__menu-list { + @extend .fr-menu__list !optional; } &__menu-notice { color: var(--grey-425-625); diff --git a/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.tsx b/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.tsx index 5585f7280f..3507ec86bd 100644 --- a/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.tsx +++ b/libs/react-design-system/src/immersionFacile/components/rs-autocomplete/RSAutocomplete.tsx @@ -9,7 +9,13 @@ export type OptionType = { value: T; label: string }; export type RSAutocompleteProps = InputProps.Common & InputProps.RegularInput & { - selectProps?: SelectProps, false, GroupBase>>; + selectProps?: SelectProps< + OptionType, + false, + GroupBase> + > & { + isDebouncing?: boolean; + }; initialInputValue?: string; }; @@ -54,26 +60,26 @@ export const RSAutocomplete = ({ classNames={{ input: () => fr.cx("fr-input", { "fr-input--error": hasError }), menu: () => cx(fr.cx("fr-menu", "fr-p-0", "fr-m-0"), Styles.menu), + menuList: () => cx(fr.cx("fr-menu__list"), Styles.menuList), + option: () => cx(fr.cx("fr-nav__link")), }} components={{ - MenuList: ({ children, innerRef }) => ( -
    - {children} -
- ), DropdownIndicator: () => null, - Option: ({ children, innerProps, innerRef }) => ( -
  • -
    - {children} -
    -
  • - ), }} - noOptionsMessage={selectProps?.noOptionsMessage} + noOptionsMessage={ + selectProps?.noOptionsMessage || + (({ inputValue }) => { + if (inputValue.length < 3) + return <>Saisissez au moins 3 caractères; + if (selectProps?.isLoading || selectProps?.isDebouncing) + return selectProps?.loadingMessage ? ( + selectProps.loadingMessage({ inputValue }) + ) : ( + <>Recherche en cours... + ); + return <>Aucune suggestion trouvée pour {inputValue}; + }) + } hideSelectedOptions isClearable id={`${selectProps?.inputId}-wrapper`} diff --git a/shared/src/naf/naf.dto.ts b/shared/src/naf/naf.dto.ts index 72d0846db5..16d4ecb166 100644 --- a/shared/src/naf/naf.dto.ts +++ b/shared/src/naf/naf.dto.ts @@ -53,7 +53,7 @@ export const nafSectorLabels: Record = { export type NafCode = Flavor; export type NafNomenclature = Flavor; -export type NafSectionLabel = Flavor; +export type NafSectionLabel = Flavor; export type NafDto = { code: NafCode; @@ -61,6 +61,7 @@ export type NafDto = { }; export type WithNafCodes = { nafCodes?: NafCode[]; + nafLabel?: NafSectionLabel; }; export const fromNafSubClassToNafClass = (nafSubClass: string): string => {