Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2848 - Ajout du filtre NAF dans les filtres de recherche (front) #2880

4 changes: 2 additions & 2 deletions back/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/multer": "^1.4.7",
"@types/node": "^20.15.0",
"@types/node": "^20.17.16",
"@types/node-fetch": "^2.6.11",
"@types/papaparse": "^5.3.7",
"@types/pg": "^8.10.2",
Expand All @@ -119,6 +119,6 @@
"supertest": "^6.2.2",
"ts-node-dev": "^2.0.0",
"ts-prune": "^0.10.3",
"typescript": "^5.6.2"
"typescript": "^5.7.3"
}
}
2 changes: 2 additions & 0 deletions back/src/scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ const establishmentSeed = async (uow: UnitOfWork) => {
])
.withSiret("34493368400021")
.withName("France Merguez Distribution")
.withNafDto({ code: "1013A", nomenclature: "rev2" }) // NAF Section :Industrie manufacturière
.build(),
)
.withOffers([
Expand All @@ -280,6 +281,7 @@ const establishmentSeed = async (uow: UnitOfWork) => {
new EstablishmentEntityBuilder()
.withSiret("50056940501696")
.withName("Decathlon france")
.withNafDto({ code: "4764Z", nomenclature: "rev2" }) // NAF Section : Commerce ; réparation d'automobiles et de motocycles
.build(),
)
.withOffers([
Expand Down
9 changes: 5 additions & 4 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
"@sentry/vite-plugin": "^2.3.0",
"@types/jest": "^29.5.2",
"@types/leaflet": "^1.9.12",
"@types/node": "^20.15.0",
"@types/node": "^20.17.16",
"@types/papaparse": "^5.3.7",
"@types/ramda": "^0.30.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.10",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-lines-ellipsis": "^0.15.1",
"@types/react-redux": "^7.1.23",
"@types/swagger-ui-react": "^4.18.0",
Expand All @@ -63,6 +63,7 @@
"react-hook-form": "^7.42.1",
"react-leaflet": "^4.2.1",
"react-redux": "^7.2.8",
"react-select": "^5.9.0",
"redux": "^4.2.0",
"redux-observable": "^2.0.0",
"rxjs": "^7.5.5",
Expand All @@ -74,7 +75,7 @@
"ts-pattern": "^5.0.0",
"tss-react": "^4.5.2",
"type-route": "^1.0.0",
"typescript": "^5.6.2",
"typescript": "^5.7.3",
"uuid": "^8.3.2",
"vite": "^5.4.14",
"vite-plugin-pwa": "^0.17.2",
Expand Down
159 changes: 43 additions & 116 deletions front/src/app/components/forms/autocomplete/AppellationAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
import { fr } from "@codegouvfr/react-dsfr";
import Autocomplete from "@mui/material/Autocomplete";
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
RSAutocomplete,
type RSAutocompleteComponentProps,
} from "react-design-system";
import {
AppellationAndRomeDto,
AppellationMatchDto,
ROME_AND_APPELLATION_MIN_SEARCH_TEXT_LENGTH,
} from "shared";
import { StringWithHighlights } from "src/app/components/forms/establishment/StringWithHighlights";
import { useDebounce } from "src/app/hooks/useDebounce";
import { outOfReduxDependencies } from "src/config/dependencies";
import { useStyles } from "tss-react/dsfr";

type AppellationAutocompleteProps = {
label: React.ReactNode;
initialValue?: AppellationAndRomeDto | undefined;
onAppellationSelected: (p: AppellationAndRomeDto) => void;
onInputClear?: () => void;
className?: string;
selectedAppellations?: AppellationAndRomeDto[];
hintText?: React.ReactNode;
placeholder?: string;
id?: string;
disabled?: boolean;
export type AppellationAutocompleteProps = RSAutocompleteComponentProps<
"appellation",
AppellationAndRomeDto
> & {
useNaturalLanguage?: boolean;
shouldClearInput?: boolean;
onAfterClearInput?: () => void;
initialValue?: AppellationAndRomeDto;
};

export const AppellationAutocomplete = ({
initialValue,
onAppellationSelected,
onInputClear,
label,
className,
selectedAppellations = [],
hintText,
placeholder,
id = "im-appellation-autocomplete",
disabled = false,
onAppellationClear,
useNaturalLanguage = false,
shouldClearInput,
onAfterClearInput,
initialValue,
...props
}: AppellationAutocompleteProps) => {
const initialOption: AppellationMatchDto | null = useMemo(
() =>
Expand All @@ -58,10 +42,8 @@ export const AppellationAutocomplete = ({
const [searchTerm, setSearchTerm] = useState<string>(
initialValue?.appellationLabel ?? "",
);
const { cx } = useStyles();
const [options, setOptions] = useState<AppellationMatchDto[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [inputHasChanged, setInputHasChanged] = useState(false);
const debounceSearchTerm = useDebounce(searchTerm);
useEffect(() => {
if (initialOption && selectedOption === null) {
Expand All @@ -85,14 +67,7 @@ export const AppellationAutocomplete = ({
sanitizedTerm,
useNaturalLanguage,
);
setOptions(
appellationOptions.filter(
(appellationOption) =>
!selectedAppellations
.map((selected) => selected.appellationCode)
.includes(appellationOption.appellation.appellationCode),
),
);
setOptions(appellationOptions);
} catch (e: any) {
// biome-ignore lint/suspicious/noConsoleLog: <explanation>
console.log("AppellationAutocomplete", e);
Expand All @@ -102,13 +77,6 @@ export const AppellationAutocomplete = ({
})();
}, [debounceSearchTerm]);

useEffect(() => {
if (shouldClearInput && onAfterClearInput) {
setSearchTerm("");
onAfterClearInput();
}
}, [shouldClearInput, onAfterClearInput]);

const noOptionText = ({
isSearching,
debounceSearchTerm,
Expand All @@ -120,81 +88,40 @@ 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é";
};
return (
<Autocomplete
disablePortal
disabled={disabled}
filterOptions={(x) => x}
options={options}
value={selectedOption}
defaultValue={initialOption}
inputValue={
inputHasChanged
? searchTerm
: initialOption?.appellation.appellationLabel
}
noOptionsText={
searchTerm
? noOptionText({
isSearching,
debounceSearchTerm,
searchTerm,
})
: "Saisissez un métier"
}
getOptionLabel={(option: AppellationMatchDto | undefined) => {
if (!option || !option.appellation) return "";
return option.appellation.appellationLabel;
}}
id={id}
renderOption={(props, option) => (
<li {...props}>
<StringWithHighlights
description={option.appellation.appellationLabel}
matchRanges={option.matchRanges}
/>
</li>
)}
onChange={(_, appellationMatch: AppellationMatchDto | null) => {
if (appellationMatch) {
setSelectedOption(appellationMatch);
setSearchTerm(appellationMatch.appellation.appellationLabel);
onAppellationSelected(appellationMatch.appellation);
}
}}
onInputChange={(_, newSearchTerm, reason) => {
if (searchTerm !== newSearchTerm && reason === "input") {
if (newSearchTerm === "") {
onInputClear?.();
<RSAutocomplete
{...props}
selectProps={{
defaultInputValue: initialValue?.appellationLabel,
isLoading: isSearching,
inputId: props.selectProps?.inputId ?? "im-select__input--appellation",
loadingMessage: () => <>Recherche de métier en cours... 🔎</>,
inputValue: searchTerm,
noOptionsMessage: () =>
noOptionText({ isSearching, debounceSearchTerm, searchTerm }),
placeholder: "Ex : Boulanger, styliste, etc.",
onChange: (searchResult, actionMeta) => {
if (
actionMeta.action === "clear" ||
actionMeta.action === "remove-value"
) {
onAppellationClear();
}
setSearchTerm(newSearchTerm);
setInputHasChanged(true);
}
}}
renderInput={(params) => {
const { id } = params;

return (
<div ref={params.InputProps.ref}>
<label className={cx(fr.cx("fr-label"), className)} htmlFor={id}>
{label}
</label>
{hintText && (
<span className={fr.cx("fr-hint-text")}>{hintText}</span>
)}
<input
{...params.inputProps}
value={searchTerm}
id={id}
className={fr.cx("fr-input")}
placeholder={placeholder}
/>
</div>
);
if (searchResult && actionMeta.action === "select-option") {
onAppellationSelected(searchResult.value);
}
},
options: options.map((option) => ({
value: option.appellation,
label: option.appellation.appellationLabel,
})),
onInputChange: (value) => {
setSearchTerm(value);
enguerranws marked this conversation as resolved.
Show resolved Hide resolved
},
}}
/>
);
Expand Down
61 changes: 61 additions & 0 deletions front/src/app/components/forms/autocomplete/NafAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from "react";
import {
RSAutocomplete,
type RSAutocompleteComponentProps,
} from "react-design-system";
import { useDispatch } from "react-redux";
import { NafSectionSuggestion } from "shared";
import { useAppSelector } from "src/app/hooks/reduxHooks";
import { nafSelectors } from "src/core-logic/domain/naf/naf.selectors";
import { nafSlice } from "src/core-logic/domain/naf/naf.slice";

export type NafAutocompleteProps = RSAutocompleteComponentProps<
"naf",
NafSectionSuggestion
>;

export const NafAutocomplete = ({
onNafSelected,
onNafClear,
...props
}: NafAutocompleteProps) => {
const dispatch = useDispatch();
const [searchTerm, setSearchTerm] = useState("");
const isLoading = useAppSelector(nafSelectors.isLoading);
const isDebouncing = useAppSelector(nafSelectors.isDebouncing);
const options = useAppSelector(nafSelectors.currentNafSections);
return (
<RSAutocomplete
{...props}
selectProps={{
isLoading,
isDebouncing,
inputValue: searchTerm,
placeholder: "Ex : Administration publique",
onChange: (nafSectionSuggestion, actionMeta) => {
if (nafSectionSuggestion && actionMeta.action === "select-option") {
onNafSelected(nafSectionSuggestion.value);
dispatch(nafSlice.actions.queryWasEmptied());
}
if (
actionMeta.action === "clear" ||
actionMeta.action === "remove-value"
) {
onNafClear();
dispatch(nafSlice.actions.queryWasEmptied());
}
},
options: options.map((option) => ({
label: option.label,
value: option,
})),
onInputChange: (value, actionMeta) => {
setSearchTerm(value);
if (actionMeta.action === "input-change") {
dispatch(nafSlice.actions.queryHasChanged(value));
}
},
}}
/>
);
};
Loading
Loading