Skip to content

Commit

Permalink
front logic for naf autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
enguerranws committed Jan 30, 2025
1 parent 2b06696 commit 26c6af9
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 29 deletions.
43 changes: 18 additions & 25 deletions front/src/app/components/forms/autocomplete/NafAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
type RSAutocompleteComponentProps,
} from "react-design-system";
import { useDispatch } from "react-redux";
import { LookupSearchResult } from "shared";
import { NafSectionSuggestion } from "shared";
import { useAppSelector } from "src/app/hooks/reduxHooks";
import { geosearchSelectors } from "src/core-logic/domain/geosearch/geosearch.selectors";
import { geosearchSlice } from "src/core-logic/domain/geosearch/geosearch.slice";
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",
LookupSearchResult
NafSectionSuggestion
>;

export const NafAutocomplete = ({
Expand All @@ -20,47 +20,40 @@ export const NafAutocomplete = ({
...props
}: NafAutocompleteProps) => {
const dispatch = useDispatch();

const [searchTerm, setSearchTerm] = useState<string>("");
const isSearching: boolean = useAppSelector(geosearchSelectors.isLoading);
const searchSuggestions = useAppSelector(geosearchSelectors.suggestions);
const options = searchSuggestions.map((suggestion) => ({
value: suggestion,
label: suggestion.label,
}));
const [searchTerm, setSearchTerm] = useState("");
const isLoading = useAppSelector(nafSelectors.isLoading);
const options = useAppSelector(nafSelectors.currentNafSections);
return (
<RSAutocomplete
{...props}
selectProps={{
isLoading: isSearching,
isLoading,
inputValue: searchTerm,
noOptionsMessage: () => <>Saisissez au moins 3 caractères</>,
placeholder: "Ex : Administration publique",
onChange: (searchResult, actionMeta) => {
if (searchResult && actionMeta.action === "select-option") {
onNafSelected(searchResult.value);
dispatch(
geosearchSlice.actions.suggestionHasBeenSelected(
searchResult.value,
),
);
onChange: (nafSectionSuggestion, actionMeta) => {
if (nafSectionSuggestion && actionMeta.action === "select-option") {
onNafSelected(nafSectionSuggestion.value);
}
if (
actionMeta.action === "clear" ||
actionMeta.action === "remove-value"
) {
onNafClear();
geosearchSlice.actions.queryWasEmptied();
dispatch(nafSlice.actions.queryWasEmptied());
}
},
options,
options: options.map((option) => ({
label: option.label,
value: option,
})),
onInputChange: (value, actionMeta) => {
setSearchTerm(value);
if (actionMeta.action === "input-change") {
dispatch(geosearchSlice.actions.queryHasChanged(value));
dispatch(nafSlice.actions.queryHasChanged(value));
if (value === "") {
onNafClear();
dispatch(geosearchSlice.actions.queryWasEmptied());
dispatch(nafSlice.actions.queryWasEmptied());
}
}
},
Expand Down
14 changes: 12 additions & 2 deletions front/src/app/pages/search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,18 @@ export const SearchPage = ({
)}
<NafAutocomplete
label="Et / ou un secteur d'activité"
onNafSelected={() => {}}
onNafClear={() => {}}
onNafSelected={(nafSectionSuggestion) => {
setTempValue({
...tempValue,
nafCodes: nafSectionSuggestion.nafCodes,
});
}}
onNafClear={() => {
setTempValue({
...tempValue,
nafCodes: undefined,
});
}}
className={fr.cx("fr-mt-2w")}
initialInputValue={"initial value"}
/>
Expand Down
3 changes: 3 additions & 0 deletions front/src/config/createHttpDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
establishmentRoutes,
formCompletionRoutes,
inclusionConnectedAllowedRoutes,
nafRoutes,
searchImmersionRoutes,
technicalRoutes,
unauthenticatedConventionRoutes,
Expand All @@ -26,6 +27,7 @@ import { HttpEstablishmentGateway } from "src/core-logic/adapters/EstablishmentG
import { HttpEstablishmentLeadGateway } from "src/core-logic/adapters/EstablishmentLeadGateway/HttpEstablishmentLeadGateway";
import { HttpFormCompletionGateway } from "src/core-logic/adapters/FormCompletionGateway/HttpFormCompletionGateway";
import { HttpInclusionConnectedGateway } from "src/core-logic/adapters/InclusionConnected/HttpInclusionConnectedGateway";
import { HttpNafGateway } from "src/core-logic/adapters/NafGateway/HttpNafGateway";
import { HttpSearchGateway } from "src/core-logic/adapters/SearchGateway/HttpSearchGateway";
import { HttpTechnicalGateway } from "src/core-logic/adapters/TechnicalGateway/HttpTechnicalGateway";

Expand Down Expand Up @@ -84,5 +86,6 @@ export const createHttpDependencies = (): Dependencies => {
createAxiosHttpClientOnSlashApi(technicalRoutes),
axiosOnSlashApi,
),
nafGateway: new HttpNafGateway(createAxiosHttpClientOnSlashApi(nafRoutes)),
};
};
2 changes: 2 additions & 0 deletions front/src/config/createInMemoryDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SimulatedEstablishmentLeadGateway } from "src/core-logic/adapters/Estab
import { SimulatedFormCompletionGateway } from "src/core-logic/adapters/FormCompletionGateway/SimulatedFormCompletionGateway";
import { seedRomeDtos } from "src/core-logic/adapters/FormCompletionGateway/TestFormCompletionGateway";
import { SimulatedInclusionConnectedGateway } from "src/core-logic/adapters/InclusionConnected/SimulatedInclusionConnectedGateway";
import { SimulatedNafGateway } from "src/core-logic/adapters/NafGateway/SimulatedNafGateway";
import { SimulatedSearchGateway } from "src/core-logic/adapters/SearchGateway/SimulatedSearchGateway";
import { seedSearchResults } from "src/core-logic/adapters/SearchGateway/simulatedSearchData";
import { SimulatedTechnicalGateway } from "src/core-logic/adapters/TechnicalGateway/SimulatedTechnicalGateway";
Expand Down Expand Up @@ -61,5 +62,6 @@ export const createInMemoryDependencies = (): Dependencies => ({
),
technicalGateway: new SimulatedTechnicalGateway(),
inclusionConnectedGateway: new SimulatedInclusionConnectedGateway(),
nafGateway: new SimulatedNafGateway(SIMULATED_LATENCY_MS),
...createCommonDependencies(),
});
2 changes: 2 additions & 0 deletions front/src/config/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { EstablishmentGateway } from "src/core-logic/ports/EstablishmentGateway"
import { EstablishmentLeadGateway } from "src/core-logic/ports/EstablishmentLeadGateway";
import { FormCompletionGateway } from "src/core-logic/ports/FormCompletionGateway";
import { InclusionConnectedGateway } from "src/core-logic/ports/InclusionConnectedGateway";
import { NafGateway } from "src/core-logic/ports/NafGateway";
import { NavigationGateway } from "src/core-logic/ports/NavigationGateway";
import { SearchGateway } from "src/core-logic/ports/SearchGateway";
import { TechnicalGateway } from "src/core-logic/ports/TechnicalGateway";
Expand All @@ -38,6 +39,7 @@ export type Dependencies = {
sessionDeviceRepository: DeviceRepository<SessionStoragePair>;
minSearchResultsToPreventRefetch: number;
scheduler: SchedulerLike;
nafGateway: NafGateway;
};

const dependencies =
Expand Down
30 changes: 30 additions & 0 deletions front/src/core-logic/adapters/NafGateway/HttpNafGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Observable, from } from "rxjs";
import { NafRoutes, NafSectionSuggestion } from "shared";
import { HttpClient } from "shared-routes";
import {
otherwiseThrow,
throwBadRequestWithExplicitMessage,
} from "src/core-logic/adapters/otherwiseThrow";
import { NafGateway } from "src/core-logic/ports/NafGateway";
import { match } from "ts-pattern";

export class HttpNafGateway implements NafGateway {
constructor(private readonly httpClient: HttpClient<NafRoutes>) {}

getNafSuggestions$(searchText: string): Observable<NafSectionSuggestion[]> {
return from(
this.httpClient
.sectionSuggestions({
queryParams: {
searchText,
},
})
.then((response) =>
match(response)
.with({ status: 200 }, ({ body }) => body)
.with({ status: 400 }, throwBadRequestWithExplicitMessage)
.otherwise(otherwiseThrow),
),
);
}
}
32 changes: 32 additions & 0 deletions front/src/core-logic/adapters/NafGateway/SimulatedNafGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Observable, Subject, delay, of } from "rxjs";
import { NafSectionSuggestion } from "shared";
import { NafGateway } from "src/core-logic/ports/NafGateway";

export class SimulatedNafGateway implements NafGateway {
public nafSuggestions$ = new Subject<NafSectionSuggestion[]>();

constructor(private readonly simulatedLatency = 0) {}

#simulatedResponse: NafSectionSuggestion[] = [
{
label: "Agriculture, sylviculture et pêche",
nafCodes: ["1000A", "1000B"],
},
{
label: "Industries extractives",
nafCodes: ["1000C", "1000D"],
},
{
label: "Industrie manufacturière",
nafCodes: ["1000E", "1000F"],
},
];

getNafSuggestions$(searchTerm: string): Observable<NafSectionSuggestion[]> {
return of(
this.#simulatedResponse.filter((suggestion) =>
suggestion.label.includes(searchTerm),
),
).pipe(delay(this.simulatedLatency));
}
}
11 changes: 11 additions & 0 deletions front/src/core-logic/adapters/NafGateway/TestNafGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Observable, Subject } from "rxjs";
import { NafSectionSuggestion } from "shared";
import { NafGateway } from "src/core-logic/ports/NafGateway";

export class TestNafGateway implements NafGateway {
public nafSuggestions$ = new Subject<NafSectionSuggestion[]>();

getNafSuggestions$(_searchTerm: string): Observable<NafSectionSuggestion[]> {
return this.nafSuggestions$;
}
}
4 changes: 2 additions & 2 deletions front/src/core-logic/domain/geosearch/geosearch.epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { geosearchSlice } from "./geosearch.slice";

type GeosearchAction = ActionOfSlice<typeof geosearchSlice>;

const queryMinLength = 3;
const debounceDuration = 500;
export const queryMinLength = 3;
export const debounceDuration = 500;

const geosearchQueryEpic: AppEpic<GeosearchAction> = (
action$,
Expand Down
46 changes: 46 additions & 0 deletions front/src/core-logic/domain/naf/naf.epics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
debounceTime,
distinctUntilChanged,
filter,
map,
switchMap,
} from "rxjs";
import { catchEpicError } from "src/core-logic/storeConfig/catchEpicError";
import {
ActionOfSlice,
AppEpic,
} from "src/core-logic/storeConfig/redux.helpers";
import { nafSlice } from "./naf.slice";

type NafAction = ActionOfSlice<typeof nafSlice>;

const queryMinLength = 3;
const debounceDuration = 500;

const queryHasChangedEpic: AppEpic<NafAction> = (
action$,
_state$,
{ scheduler },
) =>
action$.pipe(
filter(nafSlice.actions.queryHasChanged.match),
map((action) => ({ ...action, payload: action.payload.trim() })),
filter((action) => action.payload.length >= queryMinLength),
debounceTime(debounceDuration, scheduler),
distinctUntilChanged(),
map((action) => nafSlice.actions.searchSectionsRequested(action.payload)),
);

const searchSectionsEpic: AppEpic<NafAction> = (
action$,
_state$,
{ nafGateway },
) =>
action$.pipe(
filter(nafSlice.actions.searchSectionsRequested.match),
switchMap((action) => nafGateway.getNafSuggestions$(action.payload)),
map((suggestions) => nafSlice.actions.searchSectionsSucceeded(suggestions)),
catchEpicError(nafSlice.actions.searchSectionsFailed),
);

export const nafEpics = [queryHasChangedEpic, searchSectionsEpic];
16 changes: 16 additions & 0 deletions front/src/core-logic/domain/naf/naf.selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createSelector } from "@reduxjs/toolkit";
import { createRootSelector } from "src/core-logic/storeConfig/store";

const nafState = createRootSelector((state) => state.naf);

const isLoading = createSelector(nafState, (state) => state.isLoading);

const currentNafSections = createSelector(
nafState,
(state) => state.currentNafSections,
);

export const nafSelectors = {
isLoading,
currentNafSections,
};
40 changes: 40 additions & 0 deletions front/src/core-logic/domain/naf/naf.slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { NafSectionSuggestion } from "shared";

export type NafState = {
isLoading: boolean;
currentNafSections: NafSectionSuggestion[];
};

export const initialState: NafState = {
isLoading: false,
currentNafSections: [],
};

export const nafSlice = createSlice({
name: "naf",
initialState,
reducers: {
queryHasChanged: (state, _action: PayloadAction<string>) => {
state.currentNafSections = [];
},
queryWasEmptied: (state) => {
state.isLoading = false;
state.currentNafSections = [];
},
searchSectionsRequested: (state, _action: PayloadAction<string>) => {
state.isLoading = true;
},
searchSectionsSucceeded: (
state,
action: PayloadAction<NafSectionSuggestion[]>,
) => {
state.currentNafSections = action.payload;
state.isLoading = false;
},
searchSectionsFailed: (state, _action) => {
state.isLoading = false;
state.currentNafSections = [];
},
},
});
Loading

0 comments on commit 26c6af9

Please sign in to comment.