diff --git a/src/client/src/components/Search/Search.js b/src/client/src/components/Search/Search.js index e952b9097..6656b0c44 100644 --- a/src/client/src/components/Search/Search.js +++ b/src/client/src/components/Search/Search.js @@ -35,6 +35,7 @@ const Search = ({ searchTerm, setSearchTerm, resetSearch, + resetSort, handlePageChange, addFilter, removeFilter, @@ -94,7 +95,10 @@ const Search = ({ setSearchTerm={setSearchTerm} resetSearch={resetSearch} isLoading={isLoading} - onEnterPress={() => sendRequest(searchTerm, options)} + onEnterPress={() => { + resetSort(); + sendRequest(searchTerm, options); + }} />
@@ -105,6 +109,7 @@ const Search = ({ })} onClick={(e) => { e.preventDefault(); + resetSort(); sendRequest(searchTerm, options); }} > @@ -257,6 +262,7 @@ Search.propTypes = { removeFilter: PropTypes.func.isRequired, removeFilters: PropTypes.func.isRequired, resetSearch: PropTypes.func.isRequired, + resetSort: PropTypes.func.isRequired, results: PropTypes.array, searchTerm: PropTypes.string.isRequired, sendRequest: PropTypes.func.isRequired, diff --git a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js index 1d632d182..8080527c9 100644 --- a/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js +++ b/src/client/src/components/SearchAwesomeTable/SearchAwesomeTable.js @@ -1,5 +1,11 @@ import "./awesomeTable.scss"; +import { + faSort, + faSortDown, + faSortUp, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classNames from "classnames"; import PropTypes from "prop-types"; import React from "react"; @@ -9,6 +15,12 @@ import LeftArrow from "../shared/Icons/LeftArrow.jsx"; import RightArrow from "../shared/Icons/RightArrow.jsx"; import LoadSpinner from "../shared/LoadSpinner"; +const getSortIcon = (field, sortField, sortDirection) => { + if (field === sortField) { + return sortDirection === "asc" ? faSortDown : faSortUp; + } + return faSort; +}; const SearchAwesomeTable = ({ showPagination = false, pagination, @@ -17,6 +29,10 @@ const SearchAwesomeTable = ({ isLoading = false, data, fields, + // isSortable = true, + sortColumn, + sortField, + sortDirection, }) => { const handleRowClick = (event, element) => { if (event.target.tagName === "A") { @@ -29,9 +45,23 @@ const SearchAwesomeTable = ({ {fields.map((field) => { + const isSortableField = field.sortKey; return ( - + isSortableField && sortColumn(field.sortKey)} + > {field.headName} + {isSortableField && ( + + )} ); })} diff --git a/src/client/src/components/SearchResults/SearchResults.js b/src/client/src/components/SearchResults/SearchResults.js index 93365e388..cab6825e5 100644 --- a/src/client/src/components/SearchResults/SearchResults.js +++ b/src/client/src/components/SearchResults/SearchResults.js @@ -118,9 +118,7 @@ const SearchResults = ({ }); }, headName: "SIRET", - importantHead: true, link: ({ siret }) => `/establishment/${siret}`, - sortKey: "siret", }, { accessor: ({ etatAdministratifEtablissement, siret }) => { @@ -130,7 +128,7 @@ const SearchResults = ({ }); }, headName: "État", - sortKey: "etatadministratifetablissement", + sortKey: "etatAdministratifEtablissement", }, { accessor: (etablissement) => { @@ -140,7 +138,6 @@ const SearchResults = ({ }, headName: "Raison sociale / Nom", html: true, - sortKey: "enterprise_name", }, { accessor: (fields) => { @@ -172,10 +169,8 @@ const SearchResults = ({ }, headName: "Dirigeants", - importantHead: true, link: ({ fields }) => `/enterprise/${fields?.siren}#mandataires`, - sortKey: "dirigeants", }, { accessor: ({ etablissementSiege }) => { @@ -186,7 +181,6 @@ const SearchResults = ({ }); }, headName: "Catégorie établissement", - sortKey: "etablissementsiege", }, { accessor: ({ @@ -201,8 +195,8 @@ const SearchResults = ({ }); }, headName: "Code postal", - sortKey: "codepostaletablissement", }, + { accessor: ({ trancheEffectifsEtablissement, @@ -222,7 +216,7 @@ const SearchResults = ({ }); }, headName: "Effectif (DSN)", - sortKey: "lastdsntrancheeffectifsetablissement", + sortKey: "trancheEffectifsEtablissement", }, { accessor: ({ @@ -239,7 +233,7 @@ const SearchResults = ({ ); }, headName: "Activité", - sortKey: "activiteprincipaleetablissement", + sortKey: "codeActivitePrincipale", }, ]} /> diff --git a/src/client/src/containers/Search/Search.js b/src/client/src/containers/Search/Search.js index 9d5f1438f..2b17de616 100644 --- a/src/client/src/containers/Search/Search.js +++ b/src/client/src/containers/Search/Search.js @@ -2,11 +2,13 @@ import { groupBy, omit } from "lodash"; import { prop } from "lodash/fp"; import moment from "moment"; import React, { useEffect } from "react"; +import { useSelector } from "react-redux"; import SearchView from "../../components/Search"; import Http from "../../services/Http"; import { useResetSearch, + useResetSort, useSearchFilters, useSearchPage, useSearchQuery, @@ -23,7 +25,7 @@ const PAGE_SIZE = 10; const XLSX_DOC_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; -const formatLocationFilter = (filters) => { +const formatLocationFilter = (filters, sortDirection, sortField) => { const locationFilters = groupBy(filters.location, "type"); const codesCommunes = locationFilters?.commune?.map(prop("value")) || []; const departements = [ @@ -32,17 +34,26 @@ const formatLocationFilter = (filters) => { regionItem.regions.map((region) => region.value) ) || []), ]; - return { ...omit(filters, "location"), codesCommunes: normalizeCodeCommunes(codesCommunes), departements, + + sortField: sortField, + sortOrder: sortDirection, }; }; const Search = () => { const [searchQuery, setSearchQuery] = useSearchTerms(); const [searchPage, setSearchPage] = useSearchPage(); + const sortFieldFromStore = useSelector( + (state) => state?.search?.sort?.sortField + ); + + const sortOrderFromStore = useSelector( + (state) => state?.search?.sort?.sortOrder + ); const { filters, addFilter, removeFilter, removeFilters } = useSearchFilters(); @@ -50,13 +61,13 @@ const Search = () => { const { data, loading, error, makeQuery, query } = useSearchQuery(); const resetSearch = useResetSearch(); - + const resetSort = useResetSort(); useEffect(() => { if (searchQuery) { onSearch(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [sortField, sortDirection]); const downloadQuery = async () => { const trimmedQuery = searchQuery?.trim(); @@ -64,7 +75,11 @@ const Search = () => { const response = await Http.get("/downloadXlsx", { params: { q: trimmedQuery, - ...formatLocationFilter(filters), + ...formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }, responseType: "blob", }); @@ -89,7 +104,11 @@ const Search = () => { setSearchPage(nextCurrentPage); makeQuery(searchQuery, { page: { current: nextCurrentPage - 1, size: PAGE_SIZE }, - params: formatLocationFilter(filters), + params: formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }); }; @@ -97,7 +116,11 @@ const Search = () => { setSearchPage(1); makeQuery(searchQuery, { page: { current: 0, size: PAGE_SIZE }, - params: formatLocationFilter(filters), + params: formatLocationFilter( + filters, + sortOrderFromStore != null ? sortDirection : null, + sortFieldFromStore != null ? sortField : null + ), }); }; @@ -114,6 +137,7 @@ const Search = () => { searchTerm={searchQuery || ""} setSearchTerm={setSearchQuery} resetSearch={resetSearch} + resetSort={resetSort} handlePageChange={handlePageChange} addFilter={addFilter} removeFilter={removeFilter} diff --git a/src/client/src/services/Store/actions/search.js b/src/client/src/services/Store/actions/search.js index fc9f5e190..37c3b0739 100644 --- a/src/client/src/services/Store/actions/search.js +++ b/src/client/src/services/Store/actions/search.js @@ -6,6 +6,18 @@ export const setSearchTerm = (term) => (dispatch) => { type: types.SET_SEARCH_TERM, }); }; +export const setSearchSortOrder = (sortOrder) => (dispatch) => { + dispatch({ + sortOrder, + type: types.SET_SEARCH_SORT_ORDER, + }); +}; +export const setSearchSortField = (sortField) => (dispatch) => { + dispatch({ + sortField, + type: types.SET_SEARCH_SORT_FIELD, + }); +}; export const setSearchPage = (page) => (dispatch) => { dispatch({ @@ -33,3 +45,9 @@ export const resetSearch = () => (dispatch) => { type: types.RESET_SEARCH, }); }; + +export const resetSort = () => (dispatch) => { + dispatch({ + type: types.RESET_SORT, + }); +}; diff --git a/src/client/src/services/Store/constants/ActionTypes.js b/src/client/src/services/Store/constants/ActionTypes.js index 2a8fd2fd4..1709f2c8b 100644 --- a/src/client/src/services/Store/constants/ActionTypes.js +++ b/src/client/src/services/Store/constants/ActionTypes.js @@ -1,6 +1,9 @@ export const SET_SEARCH_TERM = "SET_SEARCH_TERM"; +export const SET_SEARCH_SORT_ORDER = "SET_SEARCH_SORT_ORDER"; +export const SET_SEARCH_SORT_FIELD = "SET_SEARCH_SORT_FIELD"; export const SET_SEARCH_PAGE = "SET_SEARCH_PAGE"; export const SET_SEARCH_FILTERS = "SET_SEARCH_FILTERS"; export const SET_SEARCH_SORT = "SET_SEARCH_SORT"; export const SET_SEARCH_RESULTS = "SET_SEARCH_RESULTS"; export const RESET_SEARCH = "RESET_SEARCH"; +export const RESET_SORT = "RESET_SORT"; diff --git a/src/client/src/services/Store/hooks/search.js b/src/client/src/services/Store/hooks/search.js index 5464c8679..9a363f8a3 100644 --- a/src/client/src/services/Store/hooks/search.js +++ b/src/client/src/services/Store/hooks/search.js @@ -5,9 +5,12 @@ import { useDispatch, useSelector } from "react-redux"; import { useElasticQuery } from "../../Elastic/elastic"; import { resetSearch, + resetSort, setSearchFilters, setSearchPage as setSearchPageAction, setSearchResults, + setSearchSortField as setSearchSortFieldAction, + setSearchSortOrder as setSearchSortOrderAction, setSearchTerm as setSearchTermAction, } from "../actions"; @@ -23,6 +26,17 @@ export const useSearchTerms = () => { return [searchTerm, setSearchTerm]; }; +export const useSearchSortTerms = () => { + const dispatch = useDispatch(); + const setSearchSortOrder = (term) => { + dispatch(setSearchSortOrderAction(term)); + }; + const setSearchSortField = (term) => { + dispatch(setSearchSortFieldAction(term)); + }; + + return [setSearchSortOrder, setSearchSortField]; +}; export const useSearchPage = () => { const dispatch = useDispatch(); @@ -39,12 +53,12 @@ export const useSearchFilters = () => { const dispatch = useDispatch(); const savedFilters = useSelector((state) => getSearchState(state).filters); // cache busting mechanism + const validFilters = savedFilters.etats ? savedFilters : { etats: ["A"], }; - const addFilter = (key, value) => { dispatch(setSearchFilters({ ...savedFilters, [key]: value })); }; @@ -104,3 +118,8 @@ export const useResetSearch = () => { return () => dispatch(resetSearch()); }; +export const useResetSort = () => { + const dispatch = useDispatch(); + + return () => dispatch(resetSort()); +}; diff --git a/src/client/src/services/Store/reducers/search.js b/src/client/src/services/Store/reducers/search.js index b93a13852..10d97204b 100644 --- a/src/client/src/services/Store/reducers/search.js +++ b/src/client/src/services/Store/reducers/search.js @@ -1,15 +1,20 @@ import { RESET_SEARCH, + RESET_SORT, SET_SEARCH_FILTERS, SET_SEARCH_PAGE, SET_SEARCH_RESULTS, SET_SEARCH_SORT, + SET_SEARCH_SORT_FIELD, + SET_SEARCH_SORT_ORDER, SET_SEARCH_TERM, } from "../constants/ActionTypes"; const initialState = { filters: { etats: ["A"], + sortField: null, + sortOrder: null, }, page: 1, results: { @@ -19,6 +24,8 @@ const initialState = { sort: { ascDirection: false, field: null, + sortField: null, + sortOrder: null, }, term: null, }; @@ -30,6 +37,31 @@ const search = (state = initialState, action) => { ...state, term: action.term, }; + case SET_SEARCH_SORT_ORDER: + return { + ...state, + sort: { + ...state.sort, + sortOrder: action.sortOrder, + }, + }; + case SET_SEARCH_SORT_FIELD: + return { + ...state, + sort: { + ...state.sort, + sortField: action.sortField, + }, + }; + case RESET_SORT: + return { + ...state, + sort: { + ...state.sort, + sortField: null, + sortOrder: null, + }, + }; case SET_SEARCH_PAGE: return { diff --git a/src/client/src/utils/search-table/hooks.js b/src/client/src/utils/search-table/hooks.js index 668b5e883..c8c202365 100644 --- a/src/client/src/utils/search-table/hooks.js +++ b/src/client/src/utils/search-table/hooks.js @@ -1,5 +1,8 @@ import { omit } from "lodash"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +import { useSearchSortTerms } from "../../services/Store/hooks/search"; export const useFilters = (defaultValue) => { const [filters, setFilters] = useState(defaultValue); @@ -20,22 +23,49 @@ export const useFilters = (defaultValue) => { }; export const useSort = () => { - const [sortDirection, setSortDirection] = useState("desc"); - const [sortField, setSortField] = useState(null); + const [setSearchSortOrder, setSearchSortField] = useSearchSortTerms(); + const sortFieldFromStore = useSelector( + (state) => state?.search?.sort?.sortField + ); + const sortOrderFromStore = useSelector( + (state) => state?.search?.sort?.sortOrder + ); + const [sortDirection, setSortDirection] = useState(sortOrderFromStore); + const [sortField, setSortField] = useState(sortFieldFromStore); + const { addFilter } = useFilters(); + + useEffect(() => { + if (sortFieldFromStore == null && sortOrderFromStore == null) { + setSortField(sortFieldFromStore); + setSortDirection(sortOrderFromStore); + } + }, [sortFieldFromStore, sortOrderFromStore]); const toggleSortField = (field) => { if (field !== sortField) { - setSortDirection("desc"); + // Set a new sort field and default direction to "desc" setSortField(field); - return; - } - - if (sortDirection === "desc") { - setSortDirection("asc"); - return; + setSearchSortField(field); + setSortDirection("desc"); + setSearchSortOrder("desc"); + addFilter("sortField", field); + addFilter("sortDirection", "desc"); + } else { + // Toggling sort direction if field is the same + if (sortDirection === "desc") { + setSortDirection("asc"); + setSearchSortOrder("asc"); + addFilter("sortDirection", "asc"); + } else if (sortDirection === "asc") { + // Reset on the third click + setSortField(null); + setSortDirection(null); + setSearchSortField(null); + setSearchSortOrder(null); + addFilter("sortField", null); + addFilter("sortDirection", null); + } } - - setSortField(null); }; return { diff --git a/src/server/src/utils/elastic.js b/src/server/src/utils/elastic.js index 15a00b15c..07cac3e06 100644 --- a/src/server/src/utils/elastic.js +++ b/src/server/src/utils/elastic.js @@ -1,5 +1,4 @@ import config from "config"; - import { Client } from "@elastic/elasticsearch"; import codesNaf from "@socialgouv/codes-naf"; @@ -17,7 +16,7 @@ const filtersFieldMap = { departement: "departement", codesPostaux: "codesPostalEtablissement", tranchesEffectifs: "trancheEffectifsEtablissement", - naf:"codeActivitePrincipale" + naf: "codeActivitePrincipale", }; const getCodeNafLibelle = (code) => @@ -45,27 +44,14 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { query: { bool: { must: [ - { - match: { - "dirigeants.nom": { - query: dirigeant.nom, - }, - }, - }, - { - match: { - "dirigeants.prenom": { - query: dirigeant.prenom, - }, - }, - }, + { match: { "dirigeants.nom": dirigeant.nom } }, + { match: { "dirigeants.prenom": dirigeant.prenom } }, ], }, }, }, }); - } - if (dirigeant.nom && !dirigeant.prenom) { + } else if (dirigeant.nom) { dirigeantConditions.push({ nested: { path: "dirigeants", @@ -74,8 +60,7 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { }, }, }); - } - if (dirigeant.prenom && !dirigeant.nom) { + } else if (dirigeant.prenom) { dirigeantConditions.push({ nested: { path: "dirigeants", @@ -86,6 +71,7 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { }); } } + return { ...(query ? { min_score: 20 } : {}), query: { @@ -110,7 +96,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { ] : [] ), - ...(query ? [ { @@ -129,7 +114,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { boost: 100, }, }, - { multi_match: { query, @@ -141,7 +125,6 @@ const makeQuery = ({ query, siege, dirigeant, ...filters }) => { minimum_should_match: "100%", }, }, - ...(siretOrSirenQuery ? [ { @@ -213,7 +196,7 @@ const formatElasticResult = (hit) => { export const getElasticQueryParams = (req) => { const query = (req.query["q"] || "").trim(); const activites = req.query["activites"] || []; - const naf=req.query["naf"] || []; + const naf = req.query["naf"] || []; const codesCommunes = req.query["codesCommunes"] || []; const codesPostaux = req.query["codesPostaux"] || []; const departement = req.query["departements"] || []; @@ -221,6 +204,8 @@ export const getElasticQueryParams = (req) => { const dirigeant = req.query["dirigeant"] ? JSON.parse(req.query["dirigeant"]) : null; + const sortField = req.query["sortField"]; + const sortOrder = req.query["sortOrder"]; let etats = req.query["etats"] || []; const siege = (req.query["siege"] || "").trim(); @@ -239,13 +224,26 @@ export const getElasticQueryParams = (req) => { codesPostaux, tranchesEffectifs, dirigeant, - naf + naf, + sortField, + sortOrder, }; }; export const requestElastic = async (params, { from, size }) => { - console.log(params); const body = makeQuery(params); + + // Ajouter le tri après avoir construit la requête de filtrage + if (params.sortField && params.sortOrder) { + body.sort = [ + { + [params.sortField]: { + order: params.sortOrder, + }, + }, + ]; + } + const { body: { hits: { total, hits },